Skip to content

Commit

Permalink
Merge branch 'feat-7158'
Browse files Browse the repository at this point in the history
  • Loading branch information
DavidePrincipi committed Feb 17, 2025
2 parents dfb9d28 + a87a8d7 commit 6484605
Show file tree
Hide file tree
Showing 21 changed files with 337 additions and 50 deletions.
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
35 changes: 0 additions & 35 deletions imageroot/actions/dump-custom-config/20readconfig

This file was deleted.

7 changes: 7 additions & 0 deletions imageroot/actions/get-route/validate-output.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
30 changes: 30 additions & 0 deletions imageroot/actions/get-trusted-proxies/20get_trusted_proxies
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
#!/usr/bin/env python3

#
# 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():
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,
"depth": int(os.getenv("PROXIES_DEPTH", 0))
}
json.dump(response, fp=sys.stdout)

if __name__ == "__main__":
main()
31 changes: 31 additions & 0 deletions imageroot/actions/get-trusted-proxies/validate-output.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{
"$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": {
"depth": {
"type":"integer",
"minimum": 0
},
"proxies": {
"type": "array",
"items": {
"type": "string"
}
}
}
}
28 changes: 28 additions & 0 deletions imageroot/actions/restore-module/50restore_traefik
Original file line number Diff line number Diff line change
@@ -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/

34 changes: 34 additions & 0 deletions imageroot/actions/set-route/10validate
Original file line number Diff line number Diff line change
@@ -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()
16 changes: 16 additions & 0 deletions imageroot/actions/set-route/20writeconfig
Original file line number Diff line number Diff line change
Expand Up @@ -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 } }
Expand Down
18 changes: 18 additions & 0 deletions imageroot/actions/set-route/validate-input.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
41 changes: 41 additions & 0 deletions imageroot/actions/set-trusted-proxies/20set_trusted_proxies
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
#!/usr/bin/env python3

#
# Copyright (C) 2025 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")
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):
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()
32 changes: 32 additions & 0 deletions imageroot/actions/set-trusted-proxies/validate-input.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"$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": [
{
"depth": 1,
"proxies": [
"192.168.1.1",
"192.168.1.2"
]
}
],
"type": "object",
"required": [
"proxies"
],
"additionalProperties": false,
"properties": {
"depth": {
"type":"integer",
"minimum": 0
},
"proxies": {
"type": "array",
"items": {
"type": "string"
}
}
}
}
12 changes: 12 additions & 0 deletions imageroot/actions/upload-certificate/21validate_certificates
Original file line number Diff line number Diff line change
Expand Up @@ -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
6 changes: 3 additions & 3 deletions imageroot/bin/module-cleanup-state
Original file line number Diff line number Diff line change
@@ -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
17 changes: 17 additions & 0 deletions imageroot/bin/module-dump-state
Original file line number Diff line number Diff line change
@@ -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/
6 changes: 6 additions & 0 deletions imageroot/etc/state-include.conf
Original file line number Diff line number Diff line change
@@ -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
20 changes: 20 additions & 0 deletions imageroot/pypkg/conf_helpers.py
Original file line number Diff line number Diff line change
@@ -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
Loading

0 comments on commit 6484605

Please sign in to comment.