From 691a1293e4b250aef8b1cb0a647fcee49b761a5b Mon Sep 17 00:00:00 2001 From: Davide Principi Date: Fri, 7 Feb 2025 16:33:43 +0100 Subject: [PATCH 1/9] feat: verify chain of uploaded certificate Implement additional validation of uploaded certificate. The validation type can be controlled by UPLOAD_CERTIFICATE_VERIFY_TYPE environment variable. The default behavior checks if the uploaded certificate file, and any chain certificate appended to it are valid for the host CA certificate store. --- README.md | 14 ++++++++++++++ .../upload-certificate/21validate_certificates | 12 ++++++++++++ 2 files changed, 26 insertions(+) diff --git a/README.md b/README.md index 8ff3de5..689f855 100644 --- a/README.md +++ b/README.md @@ -341,3 +341,17 @@ Example: ``` api-cli run module/traefik1/upload-certificate --data '{"certFile":"LS0tLS1CRUdJTiBSU0EgU...","keyFile":"LS0tLS1CRUdJTiBSU0EgU..."}' ``` + +The action verifies whether the certificate is valid. The type of +verification is controlled by the following environment settings: + +- `UPLOAD_CERTIFICATE_VERIFY_TYPE=chain` (default) – The certificate must + be valid according to the host CA certificate store. The uploaded file + may include an intermediate CA certificate appended to the certificate + itself. + +- `UPLOAD_CERTIFICATE_VERIFY_TYPE=selfsign` – The certificate can be + self-signed or include a full chain of certificates. + +- `UPLOAD_CERTIFICATE_VERIFY_TYPE=none` – Certificate verification is + skipped. Use this value to disable expiration date checks. diff --git a/imageroot/actions/upload-certificate/21validate_certificates b/imageroot/actions/upload-certificate/21validate_certificates index 4ed2dbe..c7bb8b5 100755 --- a/imageroot/actions/upload-certificate/21validate_certificates +++ b/imageroot/actions/upload-certificate/21validate_certificates @@ -67,3 +67,15 @@ if [ "$cert_public_key" != "$key_public_key" ]; then del_certs exit 3 fi + +# Set default certificate verification type: +: "${UPLOAD_CERTIFICATE_VERIFY_TYPE:=chain}" +if [ "${UPLOAD_CERTIFICATE_VERIFY_TYPE}" = chain ] && ! openssl verify -untrusted $CERT_FILE $CERT_FILE 1>&2 ; then + echo "set-status validation-failed" >&"${AGENT_COMFD:-2}" + printf '{"field":"certFile","parameter":"certFile","value":"","error":"cert_verification_failed_chain"}\n' + exit 33 # certificate chain verification failed +elif [ "${UPLOAD_CERTIFICATE_VERIFY_TYPE}" = selfsign ] && ! openssl verify -CAfile $CERT_FILE -untrusted $CERT_FILE $CERT_FILE 1>&2 ; then + echo "set-status validation-failed" >&"${AGENT_COMFD:-2}" + printf '{"field":"certFile","parameter":"certFile","value":"","error":"cert_verification_failed_selfsigned"}\n' + exit 34 # self-signed certificate verification failed +fi From d622b7d0c60c021a99b320452611f493e9c94f78 Mon Sep 17 00:00:00 2001 From: Davide Principi Date: Fri, 7 Feb 2025 17:23:47 +0100 Subject: [PATCH 2/9] fix(test): certificate upload strict check Repeat the upload test with and without strict certificate verification. --- tests/20_traefik_certificates_api.robot | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/20_traefik_certificates_api.robot b/tests/20_traefik_certificates_api.robot index 7acffd6..c10af8d 100644 --- a/tests/20_traefik_certificates_api.robot +++ b/tests/20_traefik_certificates_api.robot @@ -63,7 +63,13 @@ Generate a custom private and public key Set Suite Variable ${key} ${encoded_key} Set Suite Variable ${csr} ${encoded_csr} +Upload rejected for a self-signed certificate + ${response} = Run task module/traefik1/upload-certificate + ... {"keyFile": "${key}", "certFile": "${csr}"} rc_expected=33 + Upload a custom certificate + # Disable strict certificate verification: + Execute Command runagent -m traefik1 bash -c 'echo UPLOAD_CERTIFICATE_VERIFY_TYPE\=selfsign >> environment' ${response} = Run task module/traefik1/upload-certificate ... {"keyFile": "${key}", "certFile": "${csr}"} ${response} = Run task module/traefik1/get-certificate {"fqdn": "test.example.com"} From 235dd089c96d6633cc772fc4ff777162bf292ee1 Mon Sep 17 00:00:00 2001 From: Davide Principi Date: Mon, 10 Feb 2025 12:51:28 +0100 Subject: [PATCH 3/9] feat: pass custom args to Podman (#74) The PODMAN_RUN_OPTS environment variable can be set to custom values. For example it can be used to set an additional environment variable for Traefik runtime, like LEGO_CA_CERTIFICATES. Refs NethServer/dev#7300 --- imageroot/systemd/user/traefik.service | 1 + 1 file changed, 1 insertion(+) diff --git a/imageroot/systemd/user/traefik.service b/imageroot/systemd/user/traefik.service index cdbd017..37b0270 100644 --- a/imageroot/systemd/user/traefik.service +++ b/imageroot/systemd/user/traefik.service @@ -20,6 +20,7 @@ ExecStart=/usr/bin/podman run \ --volume=./selfsigned.key:/etc/traefik/selfsigned.key:z \ --volume=./configs:/etc/traefik/configs:z \ --volume=./custom_certificates:/etc/traefik/custom_certificates:z \ + $PODMAN_RUN_OPTS \ ${TRAEFIK_IMAGE} ExecStartPost=-runagent write-hosts ExecStop=/usr/bin/podman stop --ignore --cidfile %t/traefik.ctr-id -t 15 From 5021db9c46ecc62eee39e6d6a6845896f05fb297 Mon Sep 17 00:00:00 2001 From: Davide Principi Date: Fri, 14 Feb 2025 17:51:56 +0100 Subject: [PATCH 4/9] feat!: disable b2 upgrade procedure --- imageroot/update-module.d/11dir_structure | 10 ++++------ imageroot/update-module.d/30upgrade_to_beta2 | 9 +++------ 2 files changed, 7 insertions(+), 12 deletions(-) diff --git a/imageroot/update-module.d/11dir_structure b/imageroot/update-module.d/11dir_structure index c37969f..7615ad3 100755 --- a/imageroot/update-module.d/11dir_structure +++ b/imageroot/update-module.d/11dir_structure @@ -1,11 +1,9 @@ -#!/usr/bin/env sh +#!/bin/bash # -# Copyright (C) 2023 Nethesis S.r.l. +# Copyright (C) 2025 Nethesis S.r.l. # SPDX-License-Identifier: GPL-3.0-or-later # -set -e - -# Update from 0.0.12 -mkdir -vp custom_certificates configs +# Placeholder, see bug NethServer/dev#7058 +exit 0 diff --git a/imageroot/update-module.d/30upgrade_to_beta2 b/imageroot/update-module.d/30upgrade_to_beta2 index 964eb4e..7615ad3 100755 --- a/imageroot/update-module.d/30upgrade_to_beta2 +++ b/imageroot/update-module.d/30upgrade_to_beta2 @@ -1,12 +1,9 @@ #!/bin/bash # -# Copyright (C) 2023 Nethesis S.r.l. +# Copyright (C) 2025 Nethesis S.r.l. # SPDX-License-Identifier: GPL-3.0-or-later # -# Create static configuration, ignore warnings -~/.config/actions/create-module/10expandconfig 2>/dev/null -~/.config/actions/create-module/50create 2>/dev/null -# Convert existing configuration to yaml -~/.config/bin/redis2yml ${MODULE_ID} +# Placeholder, see bug NethServer/dev#7058 +exit 0 From a3a3ed10185d213e874580f7c71b40e1f3f361d4 Mon Sep 17 00:00:00 2001 From: Davide Principi Date: Fri, 14 Feb 2025 18:07:26 +0100 Subject: [PATCH 5/9] feat: get and set-trusted-proxies actions Set forwardedHeaders in Traefik static configuration --- .../get-trusted-proxies/20get_trusted_proxies | 28 ++++++++++++++ .../get-trusted-proxies/validate-output.json | 27 ++++++++++++++ .../set-trusted-proxies/20set_trusted_proxies | 37 +++++++++++++++++++ .../set-trusted-proxies/validate-input.json | 27 ++++++++++++++ imageroot/pypkg/conf_helpers.py | 20 ++++++++++ 5 files changed, 139 insertions(+) create mode 100755 imageroot/actions/get-trusted-proxies/20get_trusted_proxies create mode 100644 imageroot/actions/get-trusted-proxies/validate-output.json create mode 100755 imageroot/actions/set-trusted-proxies/20set_trusted_proxies create mode 100644 imageroot/actions/set-trusted-proxies/validate-input.json create mode 100644 imageroot/pypkg/conf_helpers.py diff --git a/imageroot/actions/get-trusted-proxies/20get_trusted_proxies b/imageroot/actions/get-trusted-proxies/20get_trusted_proxies new file mode 100755 index 0000000..fb14424 --- /dev/null +++ b/imageroot/actions/get-trusted-proxies/20get_trusted_proxies @@ -0,0 +1,28 @@ +#!/usr/bin/env python3 + +# +# Copyright (C) 2023 Nethesis S.r.l. +# SPDX-License-Identifier: GPL-3.0-or-later +# + +import agent +import json +import sys +import conf_helpers + +def main(): + curconf = conf_helpers.parse_yaml_config("traefik.yaml") + try: + proxies = list(set( + curconf['entryPoints']['http']['forwardedHeaders']["trustedIPs"] + + curconf['entryPoints']['https']['forwardedHeaders']["trustedIPs"] + )) + except KeyError: + proxies = [] + response = { + "proxies": proxies, + } + json.dump(response, fp=sys.stdout) + +if __name__ == "__main__": + main() diff --git a/imageroot/actions/get-trusted-proxies/validate-output.json b/imageroot/actions/get-trusted-proxies/validate-output.json new file mode 100644 index 0000000..b2ebf37 --- /dev/null +++ b/imageroot/actions/get-trusted-proxies/validate-output.json @@ -0,0 +1,27 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "set-trusted-proxies output", + "$id": "http://schema.nethserver.org/traefik/set-trusted-proxies-output.json", + "description": "Get the IP addresses that are trusted as front-end proxies", + "examples": [ + { + "proxies": [ + "192.168.1.1", + "192.168.1.2" + ] + } + ], + "type": "object", + "required": [ + "proxies" + ], + "additionalProperties": false, + "properties": { + "proxies": { + "type": "array", + "items": { + "type": "string" + } + } + } +} diff --git a/imageroot/actions/set-trusted-proxies/20set_trusted_proxies b/imageroot/actions/set-trusted-proxies/20set_trusted_proxies new file mode 100755 index 0000000..40e4423 --- /dev/null +++ b/imageroot/actions/set-trusted-proxies/20set_trusted_proxies @@ -0,0 +1,37 @@ +#!/usr/bin/env python3 + +# +# Copyright (C) 2023 Nethesis S.r.l. +# SPDX-License-Identifier: GPL-3.0-or-later +# + +import agent +import json +import sys +import conf_helpers +import ipaddress + +def main(): + request = json.load(sys.stdin) + validate_request(request) + curconf = conf_helpers.parse_yaml_config("traefik.yaml") + curconf['entryPoints']['http'].setdefault('forwardedHeaders', {"trustedIPs": []}) + curconf['entryPoints']['https'].setdefault('forwardedHeaders', {"trustedIPs": []}) + curconf['entryPoints']['http']['forwardedHeaders']["trustedIPs"] = request['proxies'] + curconf['entryPoints']['https']['forwardedHeaders']["trustedIPs"] = request['proxies'] + conf_helpers.write_yaml_config(curconf, "traefik.yaml") + agent.run_helper("systemctl", "--user", "restart", "traefik.service").check_returncode() + +def validate_request(request): + for ipvalue in request['proxies']: + # Check if ipvalue is a string representing IPv4 or IPv6 + try: + # IP validation + ipaddress.ip_address(ipvalue) + except ValueError: + agent.set_status('validation-failed') + json.dump([{'field':'proxies','parameter':'proxies','value': ipvalue,'error':'bad_ip_address'}], fp=sys.stdout) + sys.exit(3) + +if __name__ == "__main__": + main() diff --git a/imageroot/actions/set-trusted-proxies/validate-input.json b/imageroot/actions/set-trusted-proxies/validate-input.json new file mode 100644 index 0000000..a7cc0e5 --- /dev/null +++ b/imageroot/actions/set-trusted-proxies/validate-input.json @@ -0,0 +1,27 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "set-trusted-proxies input", + "$id": "http://schema.nethserver.org/traefik/set-trusted-proxies-input.json", + "description": "Set the IP addresses that are trusted as front-end proxies", + "examples": [ + { + "proxies": [ + "192.168.1.1", + "192.168.1.2" + ] + } + ], + "type": "object", + "required": [ + "proxies" + ], + "additionalProperties": false, + "properties": { + "proxies": { + "type": "array", + "items": { + "type": "string" + } + } + } +} diff --git a/imageroot/pypkg/conf_helpers.py b/imageroot/pypkg/conf_helpers.py new file mode 100644 index 0000000..897bddb --- /dev/null +++ b/imageroot/pypkg/conf_helpers.py @@ -0,0 +1,20 @@ +# +# Copyright (C) 2025 Nethesis S.r.l. +# SPDX-License-Identifier: GPL-3.0-or-later +# + +import os +import re +import yaml + +def write_yaml_config(conf, path): + """Safely write a configuration file.""" + with open(path + '.tmp', 'w') as fp: + fp.write(yaml.safe_dump(conf, default_flow_style=False, sort_keys=False, allow_unicode=True)) + os.rename(path + '.tmp', path) + +def parse_yaml_config(path): + """Parse a YAML configuration file.""" + with open(path, 'r') as fp: + conf = yaml.safe_load(fp) + return conf From 88ea57b9d5b7de9a1a2ad617aa13c54e366456c5 Mon Sep 17 00:00:00 2001 From: Davide Principi Date: Wed, 12 Feb 2025 18:30:28 +0100 Subject: [PATCH 6/9] feat: set depth of front-end proxy tiers The PROXY_DEPTH environment variable is passed to Traefik and can be referenced in IPAllowList strategy configuration to extract the client IP address. --- imageroot/actions/get-trusted-proxies/20get_trusted_proxies | 4 +++- imageroot/actions/get-trusted-proxies/validate-output.json | 4 ++++ imageroot/actions/set-trusted-proxies/20set_trusted_proxies | 6 +++++- imageroot/actions/set-trusted-proxies/validate-input.json | 5 +++++ imageroot/systemd/user/traefik.service | 1 + 5 files changed, 18 insertions(+), 2 deletions(-) diff --git a/imageroot/actions/get-trusted-proxies/20get_trusted_proxies b/imageroot/actions/get-trusted-proxies/20get_trusted_proxies index fb14424..e34afb0 100755 --- a/imageroot/actions/get-trusted-proxies/20get_trusted_proxies +++ b/imageroot/actions/get-trusted-proxies/20get_trusted_proxies @@ -1,13 +1,14 @@ #!/usr/bin/env python3 # -# Copyright (C) 2023 Nethesis S.r.l. +# Copyright (C) 2025 Nethesis S.r.l. # SPDX-License-Identifier: GPL-3.0-or-later # import agent import json import sys +import os import conf_helpers def main(): @@ -21,6 +22,7 @@ def main(): proxies = [] response = { "proxies": proxies, + "depth": int(os.getenv("PROXIES_DEPTH", 0)) } json.dump(response, fp=sys.stdout) diff --git a/imageroot/actions/get-trusted-proxies/validate-output.json b/imageroot/actions/get-trusted-proxies/validate-output.json index b2ebf37..1af46b6 100644 --- a/imageroot/actions/get-trusted-proxies/validate-output.json +++ b/imageroot/actions/get-trusted-proxies/validate-output.json @@ -17,6 +17,10 @@ ], "additionalProperties": false, "properties": { + "depth": { + "type":"integer", + "minimum": 0 + }, "proxies": { "type": "array", "items": { diff --git a/imageroot/actions/set-trusted-proxies/20set_trusted_proxies b/imageroot/actions/set-trusted-proxies/20set_trusted_proxies index 40e4423..c173f33 100755 --- a/imageroot/actions/set-trusted-proxies/20set_trusted_proxies +++ b/imageroot/actions/set-trusted-proxies/20set_trusted_proxies @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -# Copyright (C) 2023 Nethesis S.r.l. +# Copyright (C) 2025 Nethesis S.r.l. # SPDX-License-Identifier: GPL-3.0-or-later # @@ -20,6 +20,10 @@ def main(): curconf['entryPoints']['http']['forwardedHeaders']["trustedIPs"] = request['proxies'] curconf['entryPoints']['https']['forwardedHeaders']["trustedIPs"] = request['proxies'] conf_helpers.write_yaml_config(curconf, "traefik.yaml") + if len(request['proxies']) > 0: + agent.set_env('PROXIES_DEPTH', str(request.get('depth', 1))) + else: + agent.set_env('PROXIES_DEPTH', '0') agent.run_helper("systemctl", "--user", "restart", "traefik.service").check_returncode() def validate_request(request): diff --git a/imageroot/actions/set-trusted-proxies/validate-input.json b/imageroot/actions/set-trusted-proxies/validate-input.json index a7cc0e5..e30e788 100644 --- a/imageroot/actions/set-trusted-proxies/validate-input.json +++ b/imageroot/actions/set-trusted-proxies/validate-input.json @@ -5,6 +5,7 @@ "description": "Set the IP addresses that are trusted as front-end proxies", "examples": [ { + "depth": 1, "proxies": [ "192.168.1.1", "192.168.1.2" @@ -17,6 +18,10 @@ ], "additionalProperties": false, "properties": { + "depth": { + "type":"integer", + "minimum": 0 + }, "proxies": { "type": "array", "items": { diff --git a/imageroot/systemd/user/traefik.service b/imageroot/systemd/user/traefik.service index 37b0270..02ed072 100644 --- a/imageroot/systemd/user/traefik.service +++ b/imageroot/systemd/user/traefik.service @@ -14,6 +14,7 @@ ExecStart=/usr/bin/podman run \ --cgroups=no-conmon \ --network=host \ --replace --name=%N \ + --env=PROXIES_DEPTH \ --volume=./acme:/etc/traefik/acme:z \ --volume=./traefik.yaml:/etc/traefik/traefik.yaml:z \ --volume=./selfsigned.crt:/etc/traefik/selfsigned.crt:z \ From 9e3f7453afebb381124e73e2c284b8ed4f5a8fc9 Mon Sep 17 00:00:00 2001 From: Davide Principi Date: Mon, 10 Feb 2025 17:09:47 +0100 Subject: [PATCH 7/9] feat: add IPAllowList to route management Optionally specify the list of IPv4/v6 addresses that are authorized to connect with the service exposed by the route. --- .../actions/get-route/validate-output.json | 7 ++++ imageroot/actions/set-route/10validate | 34 +++++++++++++++++++ imageroot/actions/set-route/20writeconfig | 16 +++++++++ .../actions/set-route/validate-input.json | 18 ++++++++++ imageroot/pypkg/get_route.py | 13 +++++++ 5 files changed, 88 insertions(+) create mode 100755 imageroot/actions/set-route/10validate diff --git a/imageroot/actions/get-route/validate-output.json b/imageroot/actions/get-route/validate-output.json index 6b949f1..232cdee 100644 --- a/imageroot/actions/get-route/validate-output.json +++ b/imageroot/actions/get-route/validate-output.json @@ -136,6 +136,13 @@ "title": "User created route flag", "description": "If true, the route is flagged as manually created by a user" }, + "ip_allowlist": { + "type":"array", + "description": "List of allowed client ip addresses, in CIDR format", + "items": { + "type":"string" + } + }, "headers": { "type": "object", "title": "Headers list", diff --git a/imageroot/actions/set-route/10validate b/imageroot/actions/set-route/10validate new file mode 100755 index 0000000..713985f --- /dev/null +++ b/imageroot/actions/set-route/10validate @@ -0,0 +1,34 @@ +#!/usr/bin/env python3 + +# +# Copyright (C) 2025 Nethesis S.r.l. +# SPDX-License-Identifier: GPL-3.0-or-later +# + +import json +import sys +import os +import agent +import ipaddress + +def main(): + agent.set_weight(os.path.basename(__file__), 0) # Validation step, no task progress at all + request = json.load(sys.stdin) + if "ip_allowlist" in request: + for ipvalue in request['ip_allowlist']: + # Check if ipvalue is a string representing IPv4, IPv6, or + # CIDR: + try: + if '/' in ipvalue: + # CIDR validation + ipaddress.ip_network(ipvalue, strict=False) + else: + # IP validation + ipaddress.ip_address(ipvalue) + except ValueError: + agent.set_status('validation-failed') + json.dump([{'field':'ip_allowlist','parameter':'ip_allowlist','value': ipvalue,'error':'bad_ip_address'}], fp=sys.stdout) + sys.exit(3) + +if __name__ == "__main__": + main() diff --git a/imageroot/actions/set-route/20writeconfig b/imageroot/actions/set-route/20writeconfig index c1f0ab4..654582a 100755 --- a/imageroot/actions/set-route/20writeconfig +++ b/imageroot/actions/set-route/20writeconfig @@ -71,6 +71,22 @@ if data.get("host") is not None: if data["lets_encrypt"]: router_https["tls"]["certresolver"] = "acmeServer" +# IP addresses allowed to use the router +if data.get('ip_allowlist', []) != []: + middlewares[f'{data["instance"]}-ipallowlist'] = { + "ipAllowList": { + "sourceRange": data['ip_allowlist'], + # If X-Forwarded-For header is present skip PROXIES_DEPTH + # items to extrapolate the Client IP. See set-trusted-proxies + # action. + "ipStrategy": { + "depth": '{{ env "PROXIES_DEPTH" | default "0"}}', + } + }, + } + router_http["middlewares"].append(f'{data["instance"]}-ipallowlist') + router_https["middlewares"].append(f'{data["instance"]}-ipallowlist') + # Strip the path from the request if data.get("strip_prefix"): middlewares[f'{data["instance"]}-stripprefix'] = { "stripPrefix": { "prefixes": path } } diff --git a/imageroot/actions/set-route/validate-input.json b/imageroot/actions/set-route/validate-input.json index c3ca9f5..4789ea0 100644 --- a/imageroot/actions/set-route/validate-input.json +++ b/imageroot/actions/set-route/validate-input.json @@ -26,6 +26,17 @@ "lets_encrypt": true, "http2https": true }, + { + "instance": "module3", + "url": "http://127.0.0.0:2000", + "path": "/foo", + "lets_encrypt": true, + "http2https": true, + "ip_allowlist": [ + "192.168.13.0/24", + "10.12.21.3" + ] + }, { "instance": "module1", "url": "http://127.0.0.0:2000", @@ -126,6 +137,13 @@ "title": "User created route flag", "description": "If true, the route is flagged as manually created by a user" }, + "ip_allowlist": { + "type":"array", + "description": "List of allowed client ip addresses, in CIDR format", + "items": { + "type":"string" + } + }, "headers": { "type": "object", "title": "Headers list", diff --git a/imageroot/pypkg/get_route.py b/imageroot/pypkg/get_route.py index 3b6dca1..0989c97 100644 --- a/imageroot/pypkg/get_route.py +++ b/imageroot/pypkg/get_route.py @@ -107,6 +107,19 @@ def get_route(data, ignore_error = False): # The TLS skip certificate verification flag may be missing completely: ignore. pass + if middlewares and f'{module}-ipallowlist' in middlewares: + try: + with urllib.request.urlopen(f'http://127.0.0.1/{api_path}/api/http/middlewares/{module}-ipallowlist@file') as res: + ipallowlist_middleware = json.load(res) + except urllib.error.HTTPError as e: + raise Exception(f'Error reaching traefik daemon (middlewares): {e.reason}') + + try: + route['ip_allowlist'] = ipallowlist_middleware['ipAllowList']['sourceRange'] + except KeyError: + # ipAllowList not defined, skip + pass + except urllib.error.HTTPError as e: if e.code == 404: # If the route is not found, return an empty JSON object From 56f6dcf3841fa99fbf95c4d01c316fffef9de6eb Mon Sep 17 00:00:00 2001 From: Davide Principi Date: Thu, 13 Feb 2025 13:04:52 +0100 Subject: [PATCH 8/9] chore: remove unused action for cluster-backup --- .../actions/dump-custom-config/20readconfig | 35 ------------------- 1 file changed, 35 deletions(-) delete mode 100755 imageroot/actions/dump-custom-config/20readconfig diff --git a/imageroot/actions/dump-custom-config/20readconfig b/imageroot/actions/dump-custom-config/20readconfig deleted file mode 100755 index 26cee2c..0000000 --- a/imageroot/actions/dump-custom-config/20readconfig +++ /dev/null @@ -1,35 +0,0 @@ -#!/usr/bin/env python3 - -# -# Copyright (C) 2023 Nethesis S.r.l. -# SPDX-License-Identifier: GPL-3.0-or-later -# - -import json -import os.path -import sys -import agent -from glob import glob - -dump = {'routes':{}, 'uploaded_certificates': {}} - -# custom routes -for cfile in glob(f'configs/*.yml'): - route = os.path.basename(cfile).removesuffix('.yml') - if os.path.isfile(f'manual_flags/{route}'): - with open(cfile, 'r') as fp: - dump['routes'][route] = fp.read() - -# uploaded certificates -for key in glob(f'custom_certificates/*.key'): - cn = os.path.basename(key).removesuffix('.key') - if cn not in dump['uploaded_certificates']: - dump['uploaded_certificates'][cn] = {'crt': '', 'key': ''} - with open(key, 'r') as fsk: - dump['uploaded_certificates'][cn]['key'] = fsk.read() - with open(f'custom_certificates/{cn}.crt', 'r') as fsc: - dump['uploaded_certificates'][cn]['crt'] = fsc.read() - -# acme certificates are not saved because will be recreated by traefik on first start - -json.dump(dump, fp=sys.stdout) From a87a8d7eef0c9c0e656066567bfb2363be177c58 Mon Sep 17 00:00:00 2001 From: Davide Principi Date: Thu, 13 Feb 2025 16:07:10 +0100 Subject: [PATCH 9/9] feat: dump state for backup - Save custom certificates and custom routes in the backup for restoration. - Save also acme.json and traefik.yaml for reference: when restored, they will be removed on the next backup run. --- .../actions/restore-module/50restore_traefik | 28 +++++++++++++++++++ imageroot/bin/module-cleanup-state | 6 ++-- imageroot/bin/module-dump-state | 17 +++++++++++ imageroot/etc/state-include.conf | 6 ++++ 4 files changed, 54 insertions(+), 3 deletions(-) create mode 100755 imageroot/actions/restore-module/50restore_traefik create mode 100644 imageroot/bin/module-dump-state create mode 100644 imageroot/etc/state-include.conf diff --git a/imageroot/actions/restore-module/50restore_traefik b/imageroot/actions/restore-module/50restore_traefik new file mode 100755 index 0000000..8991789 --- /dev/null +++ b/imageroot/actions/restore-module/50restore_traefik @@ -0,0 +1,28 @@ +#!/bin/bash + +# +# Copyright (C) 2025 Nethesis S.r.l. +# SPDX-License-Identifier: GPL-3.0-or-later +# + +set -e + +shopt -s nullglob + +# Restore HTTP routes created from the UI: +for froute in state-backup/manual_flags/* ; do + route=$(basename "${froute}") + if [[ -f "state-backup/configs/${route}.yml" ]]; then + cp -vfT "state-backup/configs/${route}.yml" "configs/${route}.yml" + touch "manual_flags/${route}" + fi +done + +# Restore uploaded certificates (dynamic config): +find state-backup/configs -type f -name 'cert_*.yml' -0 | \ + xargs -0 -r -- cp -pvt configs/ + +# Restore uploaded certificates (certificates and private keys): +find state-backup/custom_certificates -type f -0 | \ + xargs -0 -r -- cp -pvt custom_certificates/ + diff --git a/imageroot/bin/module-cleanup-state b/imageroot/bin/module-cleanup-state index b88e7ea..27b8c41 100755 --- a/imageroot/bin/module-cleanup-state +++ b/imageroot/bin/module-cleanup-state @@ -1,8 +1,8 @@ -#!/usr/bin/env sh +#!/bin/bash # -# Copyright (C) 2023 Nethesis S.r.l. +# Copyright (C) 2025 Nethesis S.r.l. # SPDX-License-Identifier: GPL-3.0-or-later # -rm -fv backup-custom-routes.json +rm -rf state.backup diff --git a/imageroot/bin/module-dump-state b/imageroot/bin/module-dump-state new file mode 100644 index 0000000..f4922f2 --- /dev/null +++ b/imageroot/bin/module-dump-state @@ -0,0 +1,17 @@ +#!/bin/bash + +# +# Copyright (C) 2025 Nethesis S.r.l. +# SPDX-License-Identifier: GPL-3.0-or-later +# + +set -e + +rm -rf state-backup +mkdir -vp state-backup + +cp -pvT traefik.yaml state-backup/ +cp -prvT configs state-backup/configs/ +cp -prvT manual_flags state-backup/manual_flags/ +cp -prvT custom_certificates state-backup/custom_certificates/ +cp -prvT acme/ state-backup/acme/ diff --git a/imageroot/etc/state-include.conf b/imageroot/etc/state-include.conf new file mode 100644 index 0000000..21fb754 --- /dev/null +++ b/imageroot/etc/state-include.conf @@ -0,0 +1,6 @@ +# +# Restic include patterns for Traefik state +# Syntax reference: https://pkg.go.dev/path/filepath#Glob +# Restic --files-from: https://restic.readthedocs.io/en/stable/040_backup.html#including-files +# +state/state-backup