diff --git a/board/common/rootfs/etc/ssh/sshd_config.d/var-keys.conf b/board/common/rootfs/etc/ssh/sshd_config.d/var-keys.conf deleted file mode 100644 index 005ab1ecb..000000000 --- a/board/common/rootfs/etc/ssh/sshd_config.d/var-keys.conf +++ /dev/null @@ -1,3 +0,0 @@ -HostKey /var/lib/ssh/ssh_host_rsa_key -HostKey /var/lib/ssh/ssh_host_ecdsa_key -HostKey /var/lib/ssh/ssh_host_ed25519_key diff --git a/board/common/rootfs/usr/libexec/infix/mksshkey b/board/common/rootfs/usr/libexec/infix/mksshkey new file mode 100755 index 000000000..9cc3db9db --- /dev/null +++ b/board/common/rootfs/usr/libexec/infix/mksshkey @@ -0,0 +1,24 @@ +#!/bin/bash +# Store and convert RSA PUBLIC/PRIVATE KEYs to be able to use them in +# OpenSSHd. +set -e + +NAME="$1" +DIR="$2" +PUBLIC="$3" +PRIVATE="$4" +TMP="$(mktemp)" + +echo -e '-----BEGIN RSA PRIVATE KEY-----' > "$DIR/$NAME" +echo "$PRIVATE" >> "$DIR/$NAME" +echo -e '-----END RSA PRIVATE KEY-----' >> "$DIR/$NAME" + +echo -e "-----BEGIN RSA PUBLIC KEY-----" > "$TMP" +echo -e "$PUBLIC" >> "$TMP" +echo -e "-----END RSA PUBLIC KEY-----" >> "$TMP" + +ssh-keygen -i -m PKCS8 -f "$TMP" > "$DIR/$NAME.pub" +chmod 0600 "$DIR/$NAME.pub" +chmod 0600 "$DIR/$NAME" +chown sshd:sshd "$DIR/$NAME.pub" +chown sshd:sshd "$DIR/$NAME" diff --git a/package/skeleton-init-finit/skeleton-init-finit.mk b/package/skeleton-init-finit/skeleton-init-finit.mk index 30088ec0d..8c1f8da47 100644 --- a/package/skeleton-init-finit/skeleton-init-finit.mk +++ b/package/skeleton-init-finit/skeleton-init-finit.mk @@ -146,15 +146,6 @@ endef SKELETON_INIT_FINIT_POST_INSTALL_TARGET_HOOKS += SKELETON_INIT_FINIT_SET_MINI_SNMPD endif -# OpenSSH -ifeq ($(BR2_PACKAGE_OPENSSH),y) -define SKELETON_INIT_FINIT_SET_OPENSSH - cp $(SKELETON_INIT_FINIT_AVAILABLE)/sshd.conf $(FINIT_D)/available/ - ln -sf ../available/sshd.conf $(FINIT_D)/enabled/sshd.conf -endef -SKELETON_INIT_FINIT_POST_INSTALL_TARGET_HOOKS += SKELETON_INIT_FINIT_SET_OPENSSH -endif - ifeq ($(BR2_PACKAGE_QUAGGA),y) define SKELETON_INIT_FINIT_SET_QUAGGA cp $(SKELETON_INIT_FINIT_AVAILABLE)/quagga/zebra.conf $(FINIT_D)/available/ diff --git a/package/skeleton-init-finit/skeleton/etc/finit.d/available/sshd.conf b/package/skeleton-init-finit/skeleton/etc/finit.d/available/sshd.conf index d6804a37e..7fcc952b5 100644 --- a/package/skeleton-init-finit/skeleton/etc/finit.d/available/sshd.conf +++ b/package/skeleton-init-finit/skeleton/etc/finit.d/available/sshd.conf @@ -1,4 +1,2 @@ -task \ - [S] /usr/bin/ssh-hostkeys -- Verifying SSH host keys -service env:-/etc/default/sshd \ +service <> env:-/etc/default/sshd \ [2345] /usr/sbin/sshd -D $SSHD_OPTS -- OpenSSH daemon diff --git a/src/confd/share/factory.d/10-infix-services.json b/src/confd/share/factory.d/10-infix-services.json index d8784dd33..5e86d69c1 100644 --- a/src/confd/share/factory.d/10-infix-services.json +++ b/src/confd/share/factory.d/10-infix-services.json @@ -5,6 +5,22 @@ "infix-services:mdns": { "enabled": true }, + "infix-services:ssh": { + "enabled": true, + "listen": [ + { + "name": "ipv4", + "address": "0.0.0.0", + "port": 22 + }, + { + "name": "ipv6", + "address": "::1", + "port": 22 + } + ], + "hostkey": [ "genkey" ] + }, "infix-services:web": { "enabled": true, "console": { diff --git a/src/confd/share/failure.d/10-infix-services.json b/src/confd/share/failure.d/10-infix-services.json index b2b4bc546..4371cbb54 100644 --- a/src/confd/share/failure.d/10-infix-services.json +++ b/src/confd/share/failure.d/10-infix-services.json @@ -10,5 +10,21 @@ "restconf": { "enabled": true } + }, + "infix-services:ssh": { + "enabled": true, + "listen": [ + { + "name": "ipv4", + "address": "0.0.0.0", + "port": 22 + }, + { + "name": "ipv6", + "address": "::1", + "port": 22 + } + ], + "hostkey": [ "genkey" ] } } diff --git a/src/confd/share/test.d/10-infix-services.json b/src/confd/share/test.d/10-infix-services.json index 372149242..89b1c9090 100644 --- a/src/confd/share/test.d/10-infix-services.json +++ b/src/confd/share/test.d/10-infix-services.json @@ -7,5 +7,21 @@ "restconf": { "enabled": true } + }, + "infix-services:ssh": { + "enabled": true, + "listen": [ + { + "name": "ipv4", + "address": "0.0.0.0", + "port": 22 + }, + { + "name": "ipv6", + "address": "::", + "port": 22 + } + ], + "hostkey": [ "genkey" ] } } diff --git a/src/confd/src/infix-services.c b/src/confd/src/infix-services.c index cd72884a3..8645796a1 100644 --- a/src/confd/src/infix-services.c +++ b/src/confd/src/infix-services.c @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -13,10 +14,14 @@ #include #include "core.h" +#include #define GENERATE_ENUM(ENUM) ENUM, #define GENERATE_STRING(STRING) #STRING, +#define SSH_HOSTKEYS "/etc/ssh/hostkeys" +#define SSH_HOSTKEYS_NEXT SSH_HOSTKEYS"+" + #define FOREACH_SVC(SVC) \ SVC(none) \ SVC(ssh) \ @@ -27,6 +32,11 @@ SVC(netbrowse) \ SVC(all) /* must be last entry */ +#define SSH_BASE "/etc/ssh" +#define SSHD_CONFIG_BASE SSH_BASE "/sshd_config.d" +#define SSHD_CONFIG_LISTEN SSHD_CONFIG_BASE "/listen.conf" +#define SSHD_CONFIG_HOSTKEY SSHD_CONFIG_BASE "/host-keys.conf" + typedef enum { FOREACH_SVC(GENERATE_ENUM) } svc; @@ -135,6 +145,8 @@ static int svc_change(sr_session_ctx_t *session, sr_event_t event, const char *x ena = lydx_is_enabled(srv, "enabled"); if (systemf("initctl -nbq %s %s", ena ? "enable" : "disable", svc)) ERROR("Failed %s %s", ena ? "enabling" : "disabling", name); + if (ena) + systemf("initctl -nbq touch %s", svc); /* in case already enabled */ return put(cfg, srv); } @@ -279,6 +291,72 @@ static int restconf_change(sr_session_ctx_t *session, uint32_t sub_id, const cha return put(cfg, srv); } +static int ssh_change(sr_session_ctx_t *session, uint32_t sub_id, const char *module, + const char *xpath, sr_event_t event, unsigned request_id, void *_confd) +{ + struct lyd_node *ssh = NULL, *listen, *host_key; + sr_error_t rc = SR_ERR_OK; + sr_data_t *cfg; + FILE *fp; + + switch (event) { + case SR_EV_DONE: + return svc_change(session, event, xpath, "ssh", "sshd"); + case SR_EV_ENABLED: + case SR_EV_CHANGE: + break; + + case SR_EV_ABORT: + default: + return SR_ERR_OK; + } + + if (sr_get_data(session, xpath, 0, 0, 0, &cfg) || !cfg) { + return SR_ERR_OK; + } + ssh = cfg->tree; + + if (!lydx_is_enabled(ssh, "enabled")) { + goto out; + } + + fp = fopen(SSHD_CONFIG_HOSTKEY, "w"); + if (!fp) { + rc = SR_ERR_INTERNAL; + goto out; + } + + LY_LIST_FOR(lydx_get_child(ssh, "hostkey"), host_key) { + const char *keyname = lyd_get_value(host_key); + if (!keyname) + continue; + fprintf(fp, "HostKey %s/hostkeys/%s\n", SSH_BASE, keyname); + } + + fclose(fp); + + fp = fopen(SSHD_CONFIG_LISTEN, "w"); + if (!fp) { + rc = SR_ERR_INTERNAL; + goto out; + } + + LY_LIST_FOR(lydx_get_child(ssh, "listen"), listen) { + const char *address, *port; + int ipv6; + address = lydx_get_cattr(listen, "address"); + ipv6 = !!strchr(address, ':'); + port = lydx_get_cattr(listen, "port"); + fprintf(fp, "ListenAddress %s%s%s:%s\n", ipv6 ? "[" : "", address, ipv6 ? "]" : "", port); + } + fclose(fp); + +out: + sr_release_data(cfg); + + return rc; +} + static int web_change(sr_session_ctx_t *session, uint32_t sub_id, const char *module, const char *xpath, sr_event_t event, unsigned request_id, void *_confd) { @@ -307,6 +385,70 @@ static int web_change(sr_session_ctx_t *session, uint32_t sub_id, const char *mo return put(cfg, srv); } +/* Store SSH public/private keys */ +static int change_keystore_cb(sr_session_ctx_t *session, uint32_t sub_id, const char *module_name, + const char *xpath, sr_event_t event, uint32_t request_id, void *_) +{ + int rc = SR_ERR_OK; + sr_data_t *cfg; + struct lyd_node *changes, *change; + switch (event) { + case SR_EV_CHANGE: + case SR_EV_ENABLED: + break; + case SR_EV_ABORT: + /* Remove */ + if(fexist(SSH_HOSTKEYS_NEXT)) + remove(SSH_HOSTKEYS_NEXT); + return SR_ERR_OK; + case SR_EV_DONE: + if(fexist(SSH_HOSTKEYS_NEXT)) { + if(fexist(SSH_HOSTKEYS)) + remove(SSH_HOSTKEYS); + rename(SSH_HOSTKEYS_NEXT, SSH_HOSTKEYS); + svc_change(session, event, "/infix-services:ssh", "ssh", "sshd"); + } + return SR_ERR_OK; + + default: + return SR_ERR_OK; + } + + if (sr_get_data(session, "/ietf-keystore:keystore/asymmetric-keys//.", 0, 0, 0, &cfg) || !cfg) { + return SR_ERR_OK; + } + changes = lydx_get_descendant(cfg->tree, "keystore", "asymmetric-keys", "asymmetric-key", NULL); + + LYX_LIST_FOR_EACH(changes, change, "asymmetric-key") { + const char *name, *private_key_type, *public_key_type; + const char *private_key, *public_key; + + name = lydx_get_cattr(change, "name"); + private_key_type = lydx_get_cattr(change, "private-key-format"); + public_key_type = lydx_get_cattr(change, "public-key-format"); + + if (strcmp(private_key_type, "ietf-crypto-types:rsa-private-key-format")) { + INFO("Private key %s is not of SSH type", name); + continue; + } + + if (strcmp(public_key_type, "ietf-crypto-types:ssh-public-key-format")) { + INFO("Public key %s is not of SSH type", name); + continue; + } + private_key = lydx_get_cattr(change, "cleartext-private-key"); + public_key = lydx_get_cattr(change, "public-key"); + mkdir(SSH_HOSTKEYS_NEXT, 0600); + systemf("/usr/libexec/infix/mksshkey %s %s %s %s", name, SSH_HOSTKEYS_NEXT, public_key, private_key); + } + + if (rc != SR_ERR_OK) + return rc; + + sr_release_data(cfg); + + return SR_ERR_OK; +} int infix_services_init(struct confd *confd) { int rc; @@ -315,7 +457,8 @@ int infix_services_init(struct confd *confd) 0, mdns_change, confd, &confd->sub); REGISTER_MONITOR(confd->session, "ietf-system", "/ietf-system:system/hostname", 0, hostname_change, confd, &confd->sub); - + REGISTER_CHANGE(confd->session, "infix-services", "/infix-services:ssh", + 0, ssh_change, confd, &confd->sub); REGISTER_CHANGE(confd->session, "infix-services", "/infix-services:web", 0, web_change, confd, &confd->sub); REGISTER_CHANGE(confd->session, "infix-services", "/infix-services:web/infix-services:console", @@ -327,6 +470,10 @@ int infix_services_init(struct confd *confd) REGISTER_CHANGE(confd->session, "ieee802-dot1ab-lldp", "/ieee802-dot1ab-lldp:lldp", 0, lldp_change, confd, &confd->sub); + /* Store SSH keys */ + REGISTER_CHANGE(confd->session, "ietf-keystore", "/ietf-keystore:keystore//.", + 0, change_keystore_cb, confd, &confd->sub); + return SR_ERR_OK; fail: ERROR("init failed: %s", sr_strerror(rc)); diff --git a/src/confd/yang/confd.inc b/src/confd/yang/confd.inc index 84ee4ae89..b39a7df29 100644 --- a/src/confd/yang/confd.inc +++ b/src/confd/yang/confd.inc @@ -33,7 +33,7 @@ MODULES=( "infix-dhcp-client@2024-09-20.yang" "infix-meta@2024-10-18.yang" "infix-system@2024-09-13.yang" - "infix-services@2024-05-30.yang" + "infix-services@2024-11-26.yang" "ieee802-ethernet-interface@2019-06-21.yang" "infix-ethernet-interface@2024-02-27.yang" "infix-factory-default@2023-06-28.yang" @@ -53,7 +53,7 @@ MODULES=( "ietf-netconf-acm@2018-02-14.yang" "ietf-netconf@2013-09-29.yang -e writable-running -e candidate -e rollback-on-error -e validate -e startup -e url -e xpath -e confirmed-commit" "ietf-truststore@2023-12-28.yang -e central-truststore-supported -e certificates" - "ietf-keystore@2023-12-28.yang -e central-keystore-supported -e inline-definitions-supported -e asymmetric-keys -e symmetric-keys" + "ietf-keystore@2023-12-28.yang -e central-keystore-supported -e asymmetric-keys -e symmetric-keys" "ietf-tls-server@2023-12-28.yang -e server-ident-raw-public-key -e server-ident-x509-cert" ) diff --git a/src/confd/yang/infix-services.yang b/src/confd/yang/infix-services.yang index 92dfbf776..9cb944241 100644 --- a/src/confd/yang/infix-services.yang +++ b/src/confd/yang/infix-services.yang @@ -3,10 +3,44 @@ module infix-services { namespace "urn:ietf:params:xml:ns:yang:infix-services"; prefix infix-svc; + import ietf-inet-types { + prefix inet; + reference + "RFC 6991: Common YANG Data Types"; + } + import ietf-crypto-types { + prefix ct; + reference + "RFC AAAA: YANG Data Types and Groupings for Cryptography"; + } + + import ietf-ssh-server { + prefix ssh-srv; + } + + import ietf-ssh-common { + prefix ssh-common; + } + + import ietf-tcp-server { + prefix tcp-srv; + } + import ietf-keystore { + prefix ks; + } + organization "KernelKit"; contact "kernelkit@googlegroups.com"; description "Infix services, generic."; + revision 2024-11-26 { + description "Add support for SSH server configuration"; + reference "internal"; + } + revision 2024-06-08 { + description "Add support for RESTCONF enable/disable as a web service."; + reference "internal"; + } revision 2024-05-30 { description "Add support for RESTCONF enable/disable as a web service."; reference "internal"; @@ -36,6 +70,52 @@ module infix-services { type boolean; } } + container ssh { + + leaf enabled { + type boolean; + description "Disable or enable SSH daemon"; + must "not(.) or + (count(../listen) > 0 and + (count(../hostkey) > 0))" { + error-message "When SSH is enabled, there must be at least one listen address and port configured, and a private key must be set."; + } + } + + leaf-list hostkey { + must "not(deref(.)/../ks:public-key-format) or " + + "(derived-from-or-self(deref(.)/../ks:public-key-format, 'ct:ssh-public-key-format') and" + + "derived-from-or-self(deref(.)/../ks:private-key-format, 'ct:rsa-private-key-format'))" { + error-message "Only RSA hostkeys are supported"; + } + + description + "A reference to an symmetric key that exists in + the central keystore."; + type ks:asymmetric-key-ref; + } + + list listen { + key name; + leaf name { + type string; + } + + leaf address { + type inet:ip-address; + description + "The local IP address to listen on for incoming + SSH client connections. INADDR_ANY (0.0.0.0) or + INADDR6_ANY (0:0:0:0:0:0:0:0 a.k.a. ::) MUST be + used when the server is to listen on all IPv4 or + IPv6 addresses, respectively."; + } + leaf port { + type inet:port-number; + description "Local port for SSH daemon to listen to."; + } + } + } container web { description "Web services"; diff --git a/src/confd/yang/infix-services@2024-05-30.yang b/src/confd/yang/infix-services@2024-11-26.yang similarity index 100% rename from src/confd/yang/infix-services@2024-05-30.yang rename to src/confd/yang/infix-services@2024-11-26.yang