confd: add support for user configurable https certificate

- Add x509-public-key-format identity to crypto-types
 - Add certificate node to web services container
 - Use certificate from ietf-keystore as web cert

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
This commit is contained in:
Joachim Wiberg
2026-03-20 16:18:07 +01:00
parent f61e1580cd
commit 06361032d2
22 changed files with 291 additions and 62 deletions
@@ -1 +0,0 @@
task [S] <service/confd/ready> /usr/libexec/infix/mkcert -- Verifying self-signed https certificate
@@ -1 +0,0 @@
../available/mkcert.conf
+13 -20
View File
@@ -1,7 +1,12 @@
#!/bin/sh
# Generate a self-signed TLS certificate. Called from confd keystore
# on first boot (or factory reset) when the gencert entry has empty
# keys. Output is written to a temporary directory that confd reads
# and then removes after importing into the keystore.
KEY=/cfg/ssl/private/self-signed.key
CRT=/cfg/ssl/certs/self-signed.crt
TMPDIR=/tmp/ssl
KEY=$TMPDIR/self-signed.key
CRT=$TMPDIR/self-signed.crt
country=US
state=California
@@ -20,23 +25,11 @@ if [ -z "$cn" ]; then
fi
fi
generate()
{
mkdir -p /cfg/ssl/private /cfg/ssl/certs
chmod 700 /cfg/ssl/private
mkdir -p "$TMPDIR"
chmod 700 "$TMPDIR"
gencert --country "$country" --state "$state" --city "$city" --organisation "$org" \
--organisation-unit "$unit" --common-name "$cn" \
--out-certificate $CRT --out-key $KEY
}
gencert --country "$country" --state "$state" --city "$city" --organisation "$org" \
--organisation-unit "$unit" --common-name "$cn" \
--out-certificate "$CRT" --out-key "$KEY"
CN=$(openssl x509 -noout -subject -in "${CRT}" 2>/dev/null |sed 's/.*CN=//')
if [ -z "$CN" ] || [ "$CN" != "$cn" ]; then
generate "$cn"
fi
cp "${KEY}" "/etc/ssl/private/"
cp "${CRT}" "/etc/ssl/certs/"
initctl cond set mkcert
exit 0
exit $?
@@ -1,2 +1,2 @@
service <usr/mkcert> env:-/etc/default/nginx \
service <usr/mkcert,service/confd/ready> env:-/etc/default/nginx \
[2345] nginx -g 'daemon off;' $NGINX_ARGS -- Web server
@@ -22,6 +22,7 @@
"hostkey": [ "genkey" ]
},
"infix-services:web": {
"certificate": "gencert",
"enabled": true,
"console": {
"enabled": true
@@ -0,0 +1,28 @@
{
"ietf-keystore:keystore": {
"asymmetric-keys": {
"asymmetric-key": [
{
"name": "genkey",
"public-key-format": "infix-crypto-types:ssh-public-key-format",
"public-key": "",
"private-key-format": "infix-crypto-types:rsa-private-key-format",
"cleartext-private-key": "",
"certificates": {}
},
{
"name": "gencert",
"public-key-format": "infix-crypto-types:x509-public-key-format",
"public-key": "",
"private-key-format": "infix-crypto-types:rsa-private-key-format",
"cleartext-private-key": "",
"certificates": {
"certificate": [
{ "name": "self-signed", "cert-data": "" }
]
}
}
]
}
}
}
@@ -1,18 +1,4 @@
{
"ietf-keystore:keystore": {
"asymmetric-keys": {
"asymmetric-key": [
{
"name": "genkey",
"public-key-format": "infix-crypto-types:ssh-public-key-format",
"public-key": "",
"private-key-format": "infix-crypto-types:rsa-private-key-format",
"cleartext-private-key": "",
"certificates": {}
}
]
}
},
"ietf-netconf-server:netconf-server": {
"listen": {
"endpoints": {
+2 -1
View File
@@ -1,3 +1,4 @@
factorydir = $(pkgdatadir)/factory.d
dist_factory_DATA = 10-nacm.json 10-netconf-server.json \
dist_factory_DATA = 10-keystore.json 10-nacm.json \
10-netconf-server.json \
10-infix-services.json 10-system.json
@@ -6,6 +6,7 @@
"enabled": true
},
"infix-services:web": {
"certificate": "gencert",
"enabled": true,
"restconf": {
"enabled": true
+1
View File
@@ -0,0 +1 @@
../factory.d/10-keystore.json
+2 -1
View File
@@ -1,4 +1,5 @@
failuredir = $(pkgdatadir)/failure.d
dist_failure_DATA = 10-nacm.json 10-netconf-server.json \
dist_failure_DATA = 10-keystore.json 10-nacm.json \
10-netconf-server.json \
10-infix-services.json 10-system.json
+1
View File
@@ -0,0 +1 @@
../factory.d/10-keystore.json
+2 -2
View File
@@ -1,4 +1,4 @@
testdir = $(pkgdatadir)/test.d
dist_test_DATA = 10-nacm.json 10-netconf-server.json \
10-infix-services.json 10-system.json
dist_test_DATA = 10-keystore.json 10-nacm.json 10-netconf-server.json \
10-infix-services.json 10-system.json
+10
View File
@@ -99,6 +99,7 @@ static confd_dependency_t handle_dependencies(struct lyd_node **diff, struct lyd
dkeys = lydx_get_descendant(*diff, "keystore", "asymmetric-keys", "asymmetric-key", NULL);
LYX_LIST_FOR_EACH(dkeys, dkey, "asymmetric-key") {
struct ly_set *hostkeys;
struct lyd_node *webcert;
uint32_t i;
key_name = lydx_get_cattr(dkey, "name");
@@ -116,6 +117,15 @@ static confd_dependency_t handle_dependencies(struct lyd_node **diff, struct lyd
}
ly_set_free(hostkeys, NULL);
}
webcert = lydx_get_xpathf(config, "/infix-services:web/certificate[.='%s']", key_name);
if (webcert) {
result = add_dependencies(diff, "/infix-services:web/certificate", key_name);
if (result == CONFD_DEP_ERROR) {
ERROR("Failed to add web certificate to diff for key %s", key_name);
return result;
}
}
}
hostname = lydx_get_xpathf(*diff, "/ietf-system:system/hostname");
+5 -2
View File
@@ -31,6 +31,11 @@
#include "dagger.h"
#define SSH_HOSTKEYS "/etc/ssh/hostkeys"
#define SSH_HOSTKEYS_NEXT SSH_HOSTKEYS"+"
#define SSL_CERT_DIR "/etc/ssl/certs"
#define SSL_KEY_DIR "/etc/ssl/private"
#define CB_PRIO_PRIMARY 65535
#define CB_PRIO_PASSIVE 65000
@@ -238,8 +243,6 @@ int hardware_candidate_init(struct confd *confd);
int hardware_change(sr_session_ctx_t *session, struct lyd_node *config, struct lyd_node *diff, sr_event_t event, struct confd *confd);
/* keystore.c */
#define SSH_HOSTKEYS "/etc/ssh/hostkeys"
#define SSH_HOSTKEYS_NEXT SSH_HOSTKEYS"+"
int keystore_change(sr_session_ctx_t *session, struct lyd_node *config, struct lyd_node *diff, sr_event_t event, struct confd *confd);
/* firewall.c */
+156 -14
View File
@@ -12,6 +12,11 @@
#define XPATH_KEYSTORE_ASYM "/ietf-keystore:keystore/asymmetric-keys"
#define XPATH_KEYSTORE_SYM "/ietf-keystore:keystore/symmetric-keys"
#define SSH_PRIVATE_KEY "/tmp/ssh.key"
#define SSH_PUBLIC_KEY "/tmp/ssh.pub"
#define TLS_TMPDIR "/tmp/ssl"
#define TLS_PRIVATE_KEY TLS_TMPDIR "/self-signed.key"
#define TLS_CERTIFICATE TLS_TMPDIR "/self-signed.crt"
/* return file size */
static size_t filesz(const char *fn)
@@ -68,6 +73,7 @@ static int gen_hostkey(const char *name, struct lyd_node *change)
rc = SR_ERR_INTERNAL;
}
AUDIT("Installing SSH host key \"%s\".", name);
if (systemf("/usr/libexec/infix/mksshkey %s %s %s %s", name,
SSH_HOSTKEYS_NEXT, public_key, private_key))
rc = SR_ERR_INTERNAL;
@@ -75,6 +81,72 @@ static int gen_hostkey(const char *name, struct lyd_node *change)
return rc;
}
static int gen_webcert(const char *name, struct lyd_node *change)
{
const char *private_key, *cert_data, *certname;
struct lyd_node *certs, *cert;
int new_cert;
FILE *fp;
private_key = lydx_get_cattr(change, "cleartext-private-key");
if (!private_key || !*private_key) {
ERROR("Cannot find private key for \"%s\"", name);
return SR_ERR_OK;
}
certs = lydx_get_descendant(lyd_child(change), "certificates", "certificate", NULL);
if (!certs) {
ERROR("Cannot find any certificates for \"%s\"", name);
return SR_ERR_OK;
}
cert = certs; /* Use first certificate */
certname = lydx_get_cattr(cert, "name");
if (!certname || !*certname) {
ERROR("Cannot find certificate name for \"%s\"", name);
return SR_ERR_OK;
}
cert_data = lydx_get_cattr(cert, "cert-data");
if (!cert_data || !*cert_data) {
ERROR("Cannot find certificate data \"%s\"", name);
return SR_ERR_OK;
}
/*
* Only stop nginx (by clearing usr/mkcert) if the cert file does not
* yet exist. When updating an existing cert, nginx can keep running
* and will reload via SIGHUP from finit_reload("nginx") in web_change.
*/
new_cert = !fexistf("%s/%s.crt", SSL_CERT_DIR, certname);
if (new_cert)
erase("/run/finit/cond/usr/mkcert");
AUDIT("Installing HTTPS %s certificate \"%s\"", name, certname);
fp = fopenf("w", "%s/%s.key", SSL_KEY_DIR, certname);
if (!fp) {
ERRNO("Failed creating key file for \"%s\"", certname);
return SR_ERR_INTERNAL;
}
fprintf(fp, "-----BEGIN RSA PRIVATE KEY-----\n%s\n-----END RSA PRIVATE KEY-----\n", private_key);
fclose(fp);
systemf("chmod 600 %s/%s.key", SSL_KEY_DIR, certname);
fp = fopenf("w", "%s/%s.crt", SSL_CERT_DIR, certname);
if (!fp) {
ERRNO("Failed creating crt file for \"%s\"", certname);
return SR_ERR_INTERNAL;
}
fprintf(fp, "-----BEGIN CERTIFICATE-----\n%s\n-----END CERTIFICATE-----\n", cert_data);
fclose(fp);
if (new_cert)
symlink("/run/finit/cond/reconf", "/run/finit/cond/usr/mkcert");
return SR_ERR_OK;
}
static int keystore_update(sr_session_ctx_t *session, struct lyd_node *config, struct lyd_node *diff)
{
const char *xpath = "/ietf-keystore:keystore/asymmetric-keys/asymmetric-key";
@@ -163,6 +235,84 @@ static int keystore_update(sr_session_ctx_t *session, struct lyd_node *config, s
free(private_key_format);
}
if (list)
sr_free_values(list, count);
/* Second pass: generate X.509 certificates for TLS */
list = NULL;
count = 0;
rc = sr_get_items(session, xpath, 0, 0, &list, &count);
if (rc != SR_ERR_OK)
return 0;
for (size_t i = 0; i < count; i++) {
char *name = srx_get_str(session, "%s/name", list[i].xpath);
char *public_key_format = NULL, *private_key_format = NULL;
char *pub_key = NULL, *priv_key = NULL, *cert = NULL;
sr_val_t *entry = &list[i];
if (srx_isset(session, "%s/cleartext-private-key", entry->xpath) ||
srx_isset(session, "%s/public-key", entry->xpath))
goto next_x509;
public_key_format = srx_get_str(session, "%s/public-key-format", entry->xpath);
if (!public_key_format)
goto next_x509;
private_key_format = srx_get_str(session, "%s/private-key-format", entry->xpath);
if (!private_key_format)
goto next_x509;
if (strcmp(private_key_format, "infix-crypto-types:rsa-private-key-format") ||
strcmp(public_key_format, "infix-crypto-types:x509-public-key-format"))
goto next_x509;
NOTE("X.509 certificate (%s) does not exist, generating...", name);
if (systemf("/usr/libexec/infix/mkcert")) {
ERROR("Failed generating X.509 certificate for %s", name);
goto next_x509;
}
priv_key = filerd(TLS_PRIVATE_KEY, filesz(TLS_PRIVATE_KEY));
if (!priv_key)
goto next_x509;
pub_key = filerd(TLS_CERTIFICATE, filesz(TLS_CERTIFICATE));
if (!pub_key)
goto next_x509;
/* Use cert data also for public-key (X.509 SubjectPublicKeyInfo) */
rc = srx_set_str(session, priv_key, 0, "%s/cleartext-private-key", entry->xpath);
if (rc) {
ERROR("Failed setting private key for %s... rc: %d", name, rc);
goto next_x509;
}
rc = srx_set_str(session, pub_key, 0, "%s/public-key", entry->xpath);
if (rc != SR_ERR_OK) {
ERROR("Failed setting public key for %s... rc: %d", name, rc);
goto next_x509;
}
cert = filerd(TLS_CERTIFICATE, filesz(TLS_CERTIFICATE));
if (cert) {
rc = srx_set_str(session, cert, 0,
"%s/certificates/certificate[name='self-signed']/cert-data",
entry->xpath);
if (rc)
ERROR("Failed setting cert-data for %s... rc: %d", name, rc);
}
next_x509:
rmrf(TLS_TMPDIR);
free(public_key_format);
free(private_key_format);
free(priv_key);
free(pub_key);
free(cert);
free(name);
}
if (list)
sr_free_values(list, count);
@@ -209,21 +359,13 @@ int keystore_change(sr_session_ctx_t *session, struct lyd_node *config, struct l
changes = lydx_get_descendant(config, "keystore", "asymmetric-keys", "asymmetric-key", NULL);
LYX_LIST_FOR_EACH(changes, change, "asymmetric-key") {
const char *name = lydx_get_cattr(change, "name");
const char *type;
const char *pubfmt;
type = lydx_get_cattr(change, "private-key-format");
if (strcmp(type, "infix-crypto-types:rsa-private-key-format")) {
INFO("Private key %s is not of SSH type (%s)", name, type);
continue;
}
type = lydx_get_cattr(change, "public-key-format");
if (strcmp(type, "infix-crypto-types:ssh-public-key-format")) {
INFO("Public key %s is not of SSH type (%s)", name, type);
continue;
}
gen_hostkey(name, change);
pubfmt = lydx_get_cattr(change, "public-key-format");
if (!strcmp(pubfmt, "infix-crypto-types:ssh-public-key-format"))
gen_hostkey(name, change);
else if (!strcmp(pubfmt, "infix-crypto-types:x509-public-key-format"))
gen_webcert(name, change);
}
return rc;
+45
View File
@@ -19,6 +19,7 @@
#define GENERATE_ENUM(ENUM) ENUM,
#define GENERATE_STRING(STRING) #STRING,
#define NGINX_SSL_CONF "/etc/nginx/ssl.conf"
#define AVAHI_SVC_PATH "/etc/avahi/services"
#define LLDP_CONFIG "/etc/lldpd.d/confd.conf"
@@ -625,6 +626,48 @@ out:
}
static void web_ssl_conf(struct lyd_node *srv, struct lyd_node *config)
{
const char *keyref, *certname = "self-signed";
struct lyd_node *key, *certs;
FILE *fp;
keyref = lydx_get_cattr(srv, "certificate");
if (!keyref)
keyref = "gencert";
key = lydx_get_xpathf(config, "/ietf-keystore:keystore/asymmetric-keys"
"/asymmetric-key[name='%s']", keyref);
if (key) {
certs = lydx_get_descendant(lyd_child(key), "certificates", "certificate", NULL);
if (certs) {
const char *name = lydx_get_cattr(certs, "name");
if (name && *name)
certname = name;
}
}
fp = fopen(NGINX_SSL_CONF, "w");
if (!fp) {
ERRNO("failed creating %s", NGINX_SSL_CONF);
return;
}
fprintf(fp,
"ssl_certificate %s/%s.crt;\n"
"ssl_certificate_key %s/%s.key;\n"
"\n"
"ssl_protocols TLSv1.3 TLSv1.2;\n"
"ssl_ciphers HIGH:!aNULL:!MD5;\n"
"ssl_prefer_server_ciphers on;\n"
"\n"
"ssl_session_cache shared:SSL:1m;\n"
"ssl_session_timeout 5m;\n",
SSL_CERT_DIR, certname, SSL_KEY_DIR, certname);
fclose(fp);
}
static int web_change(sr_session_ctx_t *session, struct lyd_node *config, struct lyd_node *diff, sr_event_t event, struct confd *confd)
{
struct lyd_node *srv = NULL;
@@ -638,6 +681,8 @@ static int web_change(sr_session_ctx_t *session, struct lyd_node *config, struct
if (!cfg)
return SR_ERR_OK;
web_ssl_conf(srv, config);
ena = lydx_is_enabled(srv, "enabled");
if (ena) {
svc_enadis(srx_enabled(session, "%s/enabled", WEB_CONSOLE_XPATH), ttyd, "ttyd");
+2 -2
View File
@@ -43,13 +43,13 @@ MODULES=(
"infix-firewall-icmp-types@2025-04-26.yang"
"infix-meta@2025-12-10.yang"
"infix-system@2026-03-09.yang"
"infix-services@2026-03-04.yang"
"infix-services@2026-03-20.yang"
"ieee802-ethernet-interface@2019-06-21.yang"
"infix-ethernet-interface@2024-02-27.yang"
"infix-factory-default@2023-06-28.yang"
"infix-interfaces@2025-11-06.yang -e vlan-filtering"
"ietf-crypto-types -e cleartext-symmetric-keys"
"infix-crypto-types@2025-11-09.yang"
"infix-crypto-types@2026-02-14.yang"
"ietf-keystore -e symmetric-keys"
"infix-ntp@2026-03-09.yang"
"infix-keystore@2025-12-17.yang"
@@ -6,6 +6,9 @@ module infix-crypto-types {
prefix ct;
}
revision 2026-02-14 {
description "Add X.509 public key format for TLS certificates.";
}
revision 2025-11-09 {
description "Add Wireguard public/private key and sha";
}
@@ -44,6 +47,13 @@ module infix-crypto-types {
Used for SSH host keys.";
}
identity x509-public-key-format {
base public-key-format;
base ct:subject-public-key-info-format;
description
"X.509 SubjectPublicKeyInfo format. Used for TLS certificates.";
}
identity symmetric-key-format {
base ct:symmetric-key-format;
description
+11 -3
View File
@@ -31,9 +31,11 @@ module infix-services {
contact "kernelkit@googlegroups.com";
description "Infix services, generic.";
revision 2026-03-04 {
description "Add hostname leaf to mdns container for avahi host-name override.
Add neighbors container to mdns for mDNS-SD neighbor table.";
revision 2026-03-20 {
description "Add hostname leaf to mdns container for avahi host-name override.
Add neighbors container to mdns for mDNS-SD neighbor table.
Add certificate leaf to web container for TLS keystore reference.";
reference "internal";
}
revision 2025-12-10 {
description "Adapt to changes in final version of ietf-keystore";
@@ -266,6 +268,12 @@ module infix-services {
container web {
description "Web services";
leaf certificate {
description "Reference to asymmetric key in central keystore with an
associated certificate. By default 'gencert' is used.";
type ks:central-asymmetric-key-ref;
}
leaf enabled {
description "Enable or disable on all web services.