Skip to content

Commit

Permalink
Merge pull request #62 from NethServer/export_custom_cert
Browse files Browse the repository at this point in the history
Improve Custom Certificate Handling and Redis Storage
  • Loading branch information
Amygos authored Sep 5, 2024
2 parents a7b1db2 + 0e8ca0c commit d0aad7d
Show file tree
Hide file tree
Showing 6 changed files with 114 additions and 13 deletions.
9 changes: 6 additions & 3 deletions imageroot/actions/delete-certificate/20writeconfig
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,17 @@ 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
cert_path = f'configs/certificate-{data["fqdn"]}.yml'
if os.path.isfile(cert_path):
os.unlink(cert_path)
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")
8 changes: 8 additions & 0 deletions imageroot/actions/upload-certificate/21validate_certificates
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,14 @@ if ! openssl x509 -text -noout -in $CERT_FILE >/dev/null 2>&1; then
exit 4
fi

# check it the common name is present and is not empty
cn_name=$(openssl x509 -noout -subject -nameopt=multiline -in $CERT_FILE | sed -n 's/ *commonName *= //p')
if [ -z "$cn_name" ]; then
echo "Certificate doesn't have a common name."
del_certs
exit 5
fi

# check if cert is provided by key (we compare md5 of public keys)
cert_public_key="$(openssl x509 -noout -pubkey -in $CERT_FILE | openssl md5)"
key_public_key="$(openssl pkey -pubout -in $KEY_FILE | openssl md5)"
Expand Down
44 changes: 44 additions & 0 deletions imageroot/actions/upload-certificate/23export_certificates
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
#!/usr/bin/env python3

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

import os
import json
import agent
import sys
import subprocess
from base64 import b64decode


module_id = os.environ['MODULE_ID']
node_id = os.environ['NODE_ID']

data = json.load(sys.stdin)

# read and decode the base64 certificate and key from json payload
cert = b64decode(data["certFile"]).decode()
key = b64decode(data["keyFile"]).decode()

# read the common name from the certificate
result = subprocess.run(
['openssl', 'x509', '-noout', '-subject', '-in', '/dev/stdin', '-nameopt', 'sep_multiline', '-nameopt', 'utf8'],
input=cert,
capture_output=True,
text=True
)

subject = result.stdout
domain = subject.split("\n")[1].split("CN=")[1]

# save the certificate and key in redis
rdb = agent.redis_connect(privileged=True)
rkey = f'module/{module_id}/certificate/{domain}'
rdb.hset(rkey, mapping={"cert": cert, "key": key, "custom": "true"})

# signal the certificate-updated event
event_key = f'module/{module_id}/event/certificate-updated'
event = {"rkey": rkey, "node": node_id, "module": module_id, "domain": domain, "custom": True}
rdb.publish(event_key, json.dumps(event))
23 changes: 13 additions & 10 deletions imageroot/bin/export-certificate
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,16 @@ for info in certificates:
rkey = f'module/{module_id}/certificate/{info["domain"]["main"]}'
cur_cert = rdb.hget(rkey, 'cert')
cur_key = rdb.hget(rkey, 'key')
# save the certificate only if not exists or if has been changed
if (not cur_cert or cur_cert != info["certificate"]) or (not cur_key or cur_key != info["key"]):
print(f'Saving certificate and key to {rkey}')
rdb.hset(rkey, mapping={"cert": info["certificate"], "key": info["key"]})

# signal the certificate-updated event
event_key = f'module/{module_id}/event/certificate-updated'
print(f'Publishing event {event_key}')
event = {"rkey": rkey, "node": node_id, "module": module_id, "domain": info["domain"]}
rdb.publish(event_key, json.dumps(event))
custom = rdb.hget(rkey, 'custom')
# Skip if the certificate is custom
if not custom or custom != "true":
# save the certificate only if not exists or if has been changed
if (not cur_cert or cur_cert != info["certificate"]) or (not cur_key or cur_key != info["key"]):
print(f'Saving certificate and key to {rkey}')
rdb.hset(rkey, mapping={"cert": info["certificate"], "key": info["key"], "custom": "false"})

# signal the certificate-updated event
event_key = f'module/{module_id}/event/certificate-updated'
print(f'Publishing event {event_key}')
event = {"rkey": rkey, "node": node_id, "module": module_id, "domain": info["domain"], "custom": False}
rdb.publish(event_key, json.dumps(event))
7 changes: 7 additions & 0 deletions imageroot/pypkg/custom_certificate_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
# SPDX-License-Identifier: GPL-3.0-or-later
#


import agent
import os
from pathlib import Path

CUSTOM_CERTIFICATES_DIR = 'custom_certificates'
Expand Down Expand Up @@ -64,5 +67,9 @@ def delete_custom_certificate(fqdn):
cert_file_path.unlink()
key_file_path.unlink()
cert_config_path.unlink()
# remove the certificate and key from redis
rdb = agent.redis_connect(privileged=True)
rdb.delete(f'module/{os.environ["MODULE_ID"]}/certificate/{fqdn}')

else:
raise FileNotFoundError(f'Invalid custom certificate state for {fqdn}.')
36 changes: 36 additions & 0 deletions tests/20_traefik_certificates_api.robot
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,39 @@ Delete certificate
Get empty certificates list
${response} = Run task module/traefik1/list-certificates null
Should Be Empty ${response}

Reject a certificate with missing or empty CN field
${plain_key} = Execute Command openssl genrsa 4096
${plain_csr} = Execute Command echo "${plain_key}" \| openssl req -key /dev/stdin -x509 -sha256 -days 3650 -nodes -subj "/CN=/O=YourOrganization/OU=YourUnit" -addext "subjectAltName=DNS:test.example.com"
# base64 encode the key and csr
${encoded_key} = Execute Command echo "${plain_key}" \| base64 -w 0
${encoded_csr} = Execute Command echo "${plain_csr}" \| base64 -w 0
${response} = Run task module/traefik1/upload-certificate
... {"keyFile": "${encoded_key}", "certFile": "${encoded_csr}"} rc_expected=5 decode_json=False

Generate a custom private and public key
${plain_key} = Execute Command openssl genrsa 4096
${plain_csr} = Execute Command echo "${plain_key}" \| openssl req -key /dev/stdin -x509 -sha256 -days 3650 -nodes -subj "/CN=test.example.com" -addext "subjectAltName=DNS:test.example.com"
# base64 encode the key and csr
${encoded_key} = Execute Command echo "${plain_key}" \| base64 -w 0
${encoded_csr} = Execute Command echo "${plain_csr}" \| base64 -w 0
Set Suite Variable ${key} ${encoded_key}
Set Suite Variable ${csr} ${encoded_csr}

Upload a custom certificate
${response} = Run task module/traefik1/upload-certificate
... {"keyFile": "${key}", "certFile": "${csr}"}
${response} = Run task module/traefik1/get-certificate {"fqdn": "test.example.com"}
Should Be Equal As Strings ${response['fqdn']} test.example.com
Should Be Equal As Strings ${response['obtained']} True
Should Be Equal As Strings ${response['type']} custom
# check if the certificate is stored in redis
${response} = Execute Command redis-cli --raw EXISTS module/traefik1/certificate/test.example.com
Should Be Equal As Integers ${response} 1
${response} = Execute Command redis-cli --raw HGET module/traefik1/certificate/test.example.com custom
Should Be Equal As Strings ${response} true

Delete custom certificate
Run task module/traefik1/delete-certificate {"fqdn": "test.example.com"}
${response} = Execute Command redis-cli --raw EXISTS module/traefik1/certificate/test.example.com
Should Be Equal As Integers ${response} 0

0 comments on commit d0aad7d

Please sign in to comment.