Skip to content

Commit

Permalink
Merge pull request #72 from NethServer/feat-7158-1
Browse files Browse the repository at this point in the history
Upgrade to single ACME default certificate
  • Loading branch information
DavidePrincipi authored Feb 17, 2025
2 parents 210f94e + 54c98e5 commit dfb9d28
Show file tree
Hide file tree
Showing 21 changed files with 454 additions and 334 deletions.
20 changes: 10 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -233,12 +233,12 @@ The action takes 3 parameters:

Example:
```
api-cli run set-certificate --agent module/traefik1 --data "{\"fqdn\": \"$(hostname -f)\""
api-cli run module/traefik1/set-certificate --data '{"fqdn":"myhost.example.com","sync":false}'
```

Output:
```json
{"fqdn": "example.com", "obtained": true}
{"obtained": false}
```

## get-certificate
Expand All @@ -250,12 +250,12 @@ The action takes 1 parameter:

Example:
```
api-cli run get-certificate --agent module/traefik1 --data "{\"fqdn\": \"$(hostname -f)\""
api-cli run module/traefik1/get-certificate --data '{"fqdn":"myhost.example.com"}'
```

Output:
```
{"fqdn": "example.com", "obtained": true}
{"fqdn": "myhost.example.com", "obtained": true, "type": "internal"}
```

## delete-certificate
Expand All @@ -282,22 +282,22 @@ The action takes 1 optional parameter:

Example:
```
api-cli run list-certificates --agent module/traefik1
api-cli run module/traefik1/list-certificates
```

Output:
Output (brief format):
```json
["example.com"]
["myhost.example.com"]
```

Example list expanded:
```
api-cli run list-certificates --agent module/traefik1 --data '{"expand_list": true}'
api-cli run module/traefik1/list-certificates --data '{"expand_list": true}'
```

Output:
Output (expanded format):
```json
[{"fqdn": "example.com", "obtained": false}]
[{"fqdn": "myhost.example.com", "obtained": true, "type": "internal"}]
```

## set-acme-server
Expand Down
2 changes: 1 addition & 1 deletion build-images.sh
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ container=$(buildah from scratch)
buildah add "${container}" imageroot /imageroot
buildah add "${container}" ui /ui
buildah config --entrypoint=/ \
--label="org.nethserver.images=docker.io/traefik:v2.11.18" \
--label="org.nethserver.images=docker.io/traefik:v3.3.2" \
--label="org.nethserver.flags=core_module" \
"${container}"
buildah commit "${container}" "${repobase}/${reponame}"
Expand Down
3 changes: 3 additions & 0 deletions imageroot/actions/create-module/10expandconfig
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,7 @@ ping:
manualRouting: true
api: {}
core:
defaultRuleSyntax: v3
EOF
6 changes: 4 additions & 2 deletions imageroot/actions/create-module/50create
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ http:
middlewares:
ApiServer-stripprefix:
stripPrefix:
forceSlash: 'false'
prefixes:
- /cluster-admin
ApiServerMw2:
Expand Down Expand Up @@ -90,7 +89,7 @@ cat <<EOF > configs/_api.yml
http:
middlewares:
ApisEndpointMw0:
ipWhiteList:
IPAllowList:
sourceRange:
- 127.0.0.1
ApisEndpointMw1:
Expand All @@ -112,5 +111,8 @@ EOF
# Create uploaded certificates folder
mkdir -p custom_certificates

# Create acme storage folder
mkdir -p acme

# Enable and start the services
systemctl --user enable --now traefik.service
50 changes: 17 additions & 33 deletions imageroot/actions/delete-certificate/20writeconfig
Original file line number Diff line number Diff line change
@@ -1,44 +1,28 @@
#!/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
#

#
# Delete a Let's Encrypt certificate
# Input example:
#
# {"fqdn": "example.com"}
#

import json
import sys
import os
import cert_helpers
import agent

from custom_certificate_manager import delete_custom_certificate, list_custom_certificates

# Try to parse the stdin as JSON.
# If parsing fails, output everything to stderr
data = json.load(sys.stdin)

agent_id = os.getenv("AGENT_ID", "")
if not agent_id:
raise Exception("AGENT_ID not found inside the environemnt")

# Try to delete uploaded certificate
custom_certificate = False
for cert in list_custom_certificates():
if cert.get('fqdn') == data['fqdn']:
delete_custom_certificate(data['fqdn'])
custom_certificate = True

# Try to delete the route for obtained certificate
if not custom_certificate:
cert_path = f'configs/certificate-{data["fqdn"]}.yml'
if os.path.isfile(cert_path):
os.unlink(cert_path)

# Output valid JSON
print("true")
def main():
request = json.load(sys.stdin)
fqdn = request['fqdn']
if fqdn in cert_helpers.read_custom_cert_names():
cert_helpers.remove_custom_cert(fqdn)
elif fqdn in cert_helpers.read_default_cert_names():
cert_helpers.remove_default_certificate_name(fqdn)
else:
agent.set_status('validation-failed')
json.dump([{'field': 'fqdn','parameter':'fqdn','value': fqdn,'error':'certificate_not_found'}], fp=sys.stdout)
sys.exit(2)
json.dump(True, fp=sys.stdout)

if __name__ == "__main__":
main()
17 changes: 4 additions & 13 deletions imageroot/actions/delete-certificate/21waitsync
Original file line number Diff line number Diff line change
@@ -1,18 +1,9 @@
#!/usr/bin/env python3
#!/bin/bash

#
# Copyright (C) 2023 Nethesis S.r.l.
# Copyright (C) 2025 Nethesis S.r.l.
# SPDX-License-Identifier: GPL-3.0-or-later
#

import json
import sys
import time
from get_certificate import get_certificate

data = json.load(sys.stdin)
retry = 0

while get_certificate(data).get('fqdn') == data['fqdn'] and retry <= 10:
retry += 1
time.sleep(1)
# Placeholder, see bug NethServer/dev#7058
exit 0
36 changes: 23 additions & 13 deletions imageroot/actions/get-certificate/20readconfig
Original file line number Diff line number Diff line change
@@ -1,23 +1,33 @@
#!/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 json
import sys
import os
import cert_helpers

from custom_certificate_manager import info_custom_certificate
from get_certificate import get_certificate
def main():
request = json.load(sys.stdin)
fqdn = request['fqdn']
if fqdn in cert_helpers.read_custom_cert_names():
response = {
"fqdn": fqdn,
"type": "custom",
"obtained": True,
}
elif fqdn in cert_helpers.read_default_cert_names():
response = {
"fqdn": fqdn,
"type": "internal",
"obtained": cert_helpers.has_acmejson_name(fqdn),
}
else:
response = {}
json.dump(response, fp=sys.stdout)

# Try to parse the stdin as JSON.
# If parsing fails, output everything to stderr

data = json.load(sys.stdin)
try:
cert_info = info_custom_certificate(data['fqdn'])
except FileNotFoundError:
cert_info = get_certificate(data)

json.dump(cert_info, fp=sys.stdout)
if __name__ == "__main__":
main()
2 changes: 1 addition & 1 deletion imageroot/actions/get-certificate/validate-output.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"examples": [
{
"fqdn": "example.com",
"obtained": "true",
"obtained": true,
"type": "internal"
}
],
Expand Down
74 changes: 34 additions & 40 deletions imageroot/actions/list-certificates/20readconfig
Original file line number Diff line number Diff line change
@@ -1,50 +1,44 @@
#!/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 json
import os
import agent
import sys
import urllib.request

from custom_certificate_manager import list_custom_certificates
from get_certificate import get_certificate


api_path = os.environ["API_PATH"]

data = json.load(sys.stdin)

# Get the list of routers keys
try:
with urllib.request.urlopen(f'http://127.0.0.1/{api_path}/api/http/routers') as res:
traefik_routes = json.load(res)
except urllib.error.URLError as e:
raise Exception(f'Error reaching traefik daemon: {e.reason}') from e
certificates= []

# list routes and retrieve either main for a simple list
# or name to use it inside the traefik API and list following type and valid acme cert
for route in traefik_routes:
if "certResolver" in route.get("tls", {}) and route['status'] == 'enabled':
domains = route["tls"]["domains"]
if data != None and data.get('expand_list'):
# we do not use fqdn, we use name : certificate-sub.domain.com@file or nextcloud1-https@file
certificates.append(get_certificate({'name': route['name']}))
else:
certificates.append(domains[0]["main"])

# Retrieve custom certificate
if data != None and data.get('expand_list'):
certificates = certificates + list_custom_certificates()
else:
certificates_custom = []
for item in list_custom_certificates():
certificates_custom.append(item["fqdn"])
certificates = certificates + certificates_custom

json.dump(certificates, fp=sys.stdout)
import cert_helpers

def main():
request = json.load(sys.stdin)
# Choose the action output format brief/detailed:
if request is None or request["expand_list"] is False:
response = list_certificates_brief()
else:
response = list_certificates_detailed()
json.dump(response, fp=sys.stdout)

def list_certificates_brief():
return cert_helpers.read_default_cert_names() + cert_helpers.read_custom_cert_names()

def list_certificates_detailed():
response = []
for acmename in cert_helpers.read_default_cert_names():
response.append({
"fqdn": acmename,
"type": "internal",
"obtained": cert_helpers.has_acmejson_name(acmename),
})
for certsubject in cert_helpers.read_custom_cert_names():
response.append({
"fqdn": certsubject,
"type": "custom",
"obtained": True,
})
response.sort(key=lambda item: (item["type"], item["fqdn"]))
return response

if __name__ == "__main__":
main()
44 changes: 14 additions & 30 deletions imageroot/actions/set-certificate/20writeconfig
Original file line number Diff line number Diff line change
@@ -1,41 +1,25 @@
#!/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
#

#
# Request a let's encrypt certificate
# Input example:
# {"fqdn": "example.com"}
#

import json
import sys
import os
import uuid
import yaml

# Try to parse the stdin as JSON.
# If parsing fails, output everything to stderr
data = json.load(sys.stdin)

agent_id = os.getenv("AGENT_ID", "")
if not agent_id:
raise Exception("AGENT_ID not found inside the environemnt")
import cert_helpers

# Setup HTTP ans HTTPS routers
path = uuid.uuid4()
router = {
'entrypoints': "https",
'service': "ping@internal",
'rule' : f'Host(`{data["fqdn"]}`) && Path(`/{path}`)',
'priority': '1',
'tls': { 'domains': [{'main': data["fqdn"]}], 'certresolver': "acmeServer"}
}
def main():
request = json.load(sys.stdin)
cert_helpers.add_default_certificate_name(request['fqdn'])
if request.get('sync'):
obtained = cert_helpers.wait_acmejson_sync(timeout=request.get('sync_timeout', 120))
else:
obtained = False
json.dump({"obtained": obtained}, fp=sys.stdout)
if request.get('sync') is not None and obtained is False:
exit(2)

# Write configuration file
config = {"http": {"routers": {f'certificate-{data["fqdn"]}': router}}}
with open(f'configs/certificate-{data["fqdn"]}.yml', 'w') as fp:
fp.write(yaml.safe_dump(config, default_flow_style=False, sort_keys=False, allow_unicode=True))
if __name__ == "__main__":
main()
Loading

0 comments on commit dfb9d28

Please sign in to comment.