diff --git a/board/common/rootfs/etc/finit.d/available/mkcert.conf b/board/common/rootfs/etc/finit.d/available/mkcert.conf deleted file mode 100644 index 125e831e..00000000 --- a/board/common/rootfs/etc/finit.d/available/mkcert.conf +++ /dev/null @@ -1 +0,0 @@ -task [S] /usr/libexec/infix/mkcert -- Verifying self-signed https certificate diff --git a/board/common/rootfs/etc/finit.d/enabled/mkcert.conf b/board/common/rootfs/etc/finit.d/enabled/mkcert.conf deleted file mode 120000 index 166f9f03..00000000 --- a/board/common/rootfs/etc/finit.d/enabled/mkcert.conf +++ /dev/null @@ -1 +0,0 @@ -../available/mkcert.conf \ No newline at end of file diff --git a/board/common/rootfs/usr/libexec/infix/mkcert b/board/common/rootfs/usr/libexec/infix/mkcert index 1bebec23..6441a776 100755 --- a/board/common/rootfs/usr/libexec/infix/mkcert +++ b/board/common/rootfs/usr/libexec/infix/mkcert @@ -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 $? diff --git a/package/skeleton-init-finit/skeleton/etc/finit.d/available/nginx.conf b/package/skeleton-init-finit/skeleton/etc/finit.d/available/nginx.conf index 0286c215..2905bb11 100644 --- a/package/skeleton-init-finit/skeleton/etc/finit.d/available/nginx.conf +++ b/package/skeleton-init-finit/skeleton/etc/finit.d/available/nginx.conf @@ -1,2 +1,2 @@ -service env:-/etc/default/nginx \ +service env:-/etc/default/nginx \ [2345] nginx -g 'daemon off;' $NGINX_ARGS -- Web server diff --git a/src/confd/share/factory.d/10-infix-services.json b/src/confd/share/factory.d/10-infix-services.json index f7b36180..5b7edadb 100644 --- a/src/confd/share/factory.d/10-infix-services.json +++ b/src/confd/share/factory.d/10-infix-services.json @@ -22,6 +22,7 @@ "hostkey": [ "genkey" ] }, "infix-services:web": { + "certificate": "gencert", "enabled": true, "console": { "enabled": true diff --git a/src/confd/share/factory.d/10-keystore.json b/src/confd/share/factory.d/10-keystore.json new file mode 100644 index 00000000..e4bcc9be --- /dev/null +++ b/src/confd/share/factory.d/10-keystore.json @@ -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": "" } + ] + } + } + ] + } + } +} diff --git a/src/confd/share/factory.d/10-netconf-server.json b/src/confd/share/factory.d/10-netconf-server.json index 0dcfdd2c..1effd768 100644 --- a/src/confd/share/factory.d/10-netconf-server.json +++ b/src/confd/share/factory.d/10-netconf-server.json @@ -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": { diff --git a/src/confd/share/factory.d/Makefile.am b/src/confd/share/factory.d/Makefile.am index 72b40304..4256ab4c 100644 --- a/src/confd/share/factory.d/Makefile.am +++ b/src/confd/share/factory.d/Makefile.am @@ -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 diff --git a/src/confd/share/failure.d/10-infix-services.json b/src/confd/share/failure.d/10-infix-services.json index a5ef5102..c34a89de 100644 --- a/src/confd/share/failure.d/10-infix-services.json +++ b/src/confd/share/failure.d/10-infix-services.json @@ -6,6 +6,7 @@ "enabled": true }, "infix-services:web": { + "certificate": "gencert", "enabled": true, "restconf": { "enabled": true diff --git a/src/confd/share/failure.d/10-keystore.json b/src/confd/share/failure.d/10-keystore.json new file mode 120000 index 00000000..ae64a9ee --- /dev/null +++ b/src/confd/share/failure.d/10-keystore.json @@ -0,0 +1 @@ +../factory.d/10-keystore.json \ No newline at end of file diff --git a/src/confd/share/failure.d/Makefile.am b/src/confd/share/failure.d/Makefile.am index ae981ac3..3b1f5f7d 100644 --- a/src/confd/share/failure.d/Makefile.am +++ b/src/confd/share/failure.d/Makefile.am @@ -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 diff --git a/src/confd/share/test.d/10-keystore.json b/src/confd/share/test.d/10-keystore.json new file mode 120000 index 00000000..ae64a9ee --- /dev/null +++ b/src/confd/share/test.d/10-keystore.json @@ -0,0 +1 @@ +../factory.d/10-keystore.json \ No newline at end of file diff --git a/src/confd/share/test.d/Makefile.am b/src/confd/share/test.d/Makefile.am index 89447af7..66ac915f 100644 --- a/src/confd/share/test.d/Makefile.am +++ b/src/confd/share/test.d/Makefile.am @@ -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 diff --git a/src/confd/src/core.c b/src/confd/src/core.c index b2bdc843..ef958985 100644 --- a/src/confd/src/core.c +++ b/src/confd/src/core.c @@ -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"); diff --git a/src/confd/src/core.h b/src/confd/src/core.h index e65f2705..928584fe 100644 --- a/src/confd/src/core.h +++ b/src/confd/src/core.h @@ -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 */ diff --git a/src/confd/src/keystore.c b/src/confd/src/keystore.c index 6f493e5c..a0529784 100644 --- a/src/confd/src/keystore.c +++ b/src/confd/src/keystore.c @@ -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; diff --git a/src/confd/src/services.c b/src/confd/src/services.c index 0f487f13..534616aa 100644 --- a/src/confd/src/services.c +++ b/src/confd/src/services.c @@ -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"); diff --git a/src/confd/yang/confd.inc b/src/confd/yang/confd.inc index 6f9c6af5..27bc3fe9 100644 --- a/src/confd/yang/confd.inc +++ b/src/confd/yang/confd.inc @@ -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" diff --git a/src/confd/yang/confd/infix-crypto-types.yang b/src/confd/yang/confd/infix-crypto-types.yang index ab7ab37a..ada63afa 100644 --- a/src/confd/yang/confd/infix-crypto-types.yang +++ b/src/confd/yang/confd/infix-crypto-types.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 diff --git a/src/confd/yang/confd/infix-crypto-types@2025-11-09.yang b/src/confd/yang/confd/infix-crypto-types@2026-02-14.yang similarity index 100% rename from src/confd/yang/confd/infix-crypto-types@2025-11-09.yang rename to src/confd/yang/confd/infix-crypto-types@2026-02-14.yang diff --git a/src/confd/yang/confd/infix-services.yang b/src/confd/yang/confd/infix-services.yang index 4f87b5bb..7d64f848 100644 --- a/src/confd/yang/confd/infix-services.yang +++ b/src/confd/yang/confd/infix-services.yang @@ -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. diff --git a/src/confd/yang/confd/infix-services@2026-03-04.yang b/src/confd/yang/confd/infix-services@2026-03-20.yang similarity index 100% rename from src/confd/yang/confd/infix-services@2026-03-04.yang rename to src/confd/yang/confd/infix-services@2026-03-20.yang