-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat!: single default ACME certificate
- Reimplement {set,get,delete}-certificate actions to request a single certificate with SANs. Use Traefik's defaultGeneratedCert. - Change list-certificates to list the ACME certificate host names and the custom/uploaded certs. BREAKING CHANGE: the Traefik configuration does not create certificate routers any more. Action data format is unchanged.
- Loading branch information
1 parent
3dde512
commit b80d9cf
Showing
11 changed files
with
274 additions
and
314 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,44 +1,27 @@ | ||
#!/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 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") | ||
import cert_helpers | ||
|
||
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() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,7 +6,7 @@ | |
"examples": [ | ||
{ | ||
"fqdn": "example.com", | ||
"obtained": "true", | ||
"obtained": true, | ||
"type": "internal" | ||
} | ||
], | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 HTTPS router | ||
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() |
36 changes: 4 additions & 32 deletions
36
imageroot/actions/set-certificate/21waitsync
100755 → 100644
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,37 +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 | ||
import agent | ||
from get_certificate import get_certificate | ||
|
||
data = json.load(sys.stdin) | ||
retry = 0 | ||
certificate = {} | ||
|
||
sync_timeout = data['sync_timeout'] if data.get('sync_timeout') is not None else 120 | ||
|
||
while get_certificate(data).get('fqdn') != data['fqdn'] and retry <= 10: | ||
retry += 1 | ||
time.sleep(1) | ||
|
||
certificate['obtained'] = get_certificate(data).get('obtained') | ||
|
||
if certificate['obtained'] is False and data.get('sync') is not None and data['sync'] is True: | ||
retry = 0 | ||
while certificate['obtained'] != True and retry < sync_timeout: | ||
agent.set_progress(round((retry*100)/sync_timeout)) | ||
certificate['obtained'] = get_certificate(data).get('obtained') | ||
retry += 1 | ||
time.sleep(1) | ||
|
||
json.dump(certificate, fp=sys.stdout) | ||
|
||
if data.get('sync') is not None and certificate['obtained'] is False: | ||
exit(2) | ||
# Placeholder, see bug NethServer/dev#7058 | ||
exit 0 |
Oops, something went wrong.