Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Upgrade to single ACME default certificate #72

Merged
merged 11 commits into from
Feb 17, 2025
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),
}
Comment on lines +16 to +27
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens if a custom certificate with the same name as one requested via set-certificate is uploaded?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It should override the one from LE. At least from my tests it seems Traefik gives it priority over the default generated cert.

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
Loading