Add support for configurable SSH server

This commit is contained in:
Mattias Walström
2024-12-19 14:42:17 +01:00
parent 2e81e99224
commit 37b97d9e4c
11 changed files with 288 additions and 18 deletions
@@ -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
+24
View File
@@ -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"
@@ -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/
@@ -1,4 +1,2 @@
task <pid/syslogd> \
[S] /usr/bin/ssh-hostkeys -- Verifying SSH host keys
service <task/ssh-hostkeys/success> env:-/etc/default/sshd \
service <pid/syslogd> env:-/etc/default/sshd \
[2345] /usr/sbin/sshd -D $SSHD_OPTS -- OpenSSH daemon
@@ -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": {
@@ -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" ]
}
}
@@ -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" ]
}
}
+151 -1
View File
@@ -9,14 +9,19 @@
#include <sys/types.h>
#include <srx/common.h>
#include <srx/helpers.h>
#include <srx/lyx.h>
#include <srx/srx_val.h>
#include "core.h"
#include <sysrepo_types.h>
#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);
}
@@ -342,6 +354,74 @@ 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)
{
@@ -370,6 +450,71 @@ 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))
rmrf(SSH_HOSTKEYS_NEXT);
return SR_ERR_OK;
case SR_EV_DONE:
if(fexist(SSH_HOSTKEYS_NEXT)) {
if(fexist(SSH_HOSTKEYS))
if(rmrf(SSH_HOSTKEYS)) {
ERROR("Failed to remove old SSH hostkeys: %d", errno);
}
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);
if(systemf("/usr/libexec/infix/mksshkey %s %s %s %s", name, SSH_HOSTKEYS_NEXT, public_key, private_key))
rc = SR_ERR_INTERNAL;
}
sr_release_data(cfg);
return rc;
}
int infix_services_init(struct confd *confd)
{
int rc;
@@ -378,7 +523,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",
@@ -390,6 +536,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));
+2 -2
View File
@@ -33,7 +33,7 @@ MODULES=(
"infix-dhcp-client@2024-09-20.yang"
"infix-meta@2024-10-18.yang"
"infix-system@2024-11-27.yang"
"infix-services@2024-12-02.yang"
"infix-services@2024-12-03.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"
)
+62
View File
@@ -8,16 +8,36 @@ module infix-services {
}
import ietf-inet-types {
prefix inet;
reference
"RFC 6991: Common YANG Data Types";
}
import ietf-crypto-types {
prefix ct;
reference
"RFC 9640: YANG Data Types and Groupings for Cryptography";
}
import ietf-keystore {
prefix ks;
}
organization "KernelKit";
contact "kernelkit@googlegroups.com";
description "Infix services, generic.";
revision 2024-12-03 {
description "Add support for SSH server configuration";
reference "internal";
}
revision 2024-12-02 {
description "Expand mdns options: domain, allow/deny interfaces, reflector.";
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";
@@ -104,6 +124,48 @@ module infix-services {
}
}
}
container ssh {
description "Configuration for the SSH daemon";
leaf enabled {
description "Enable or disable SSH daemon";
type boolean;
must ". = 'false' or (count(../listen) > 0 and count(../hostkey) > 0)" {
error-message "Must be at least one listen address and a private key.";
}
}
leaf-list hostkey {
description "Reference to asymmetric key in central keystore.
The hostkey can be shared with NETCONF, by default 'genkey' is used.";
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.";
}
type ks:asymmetric-key-ref;
}
list listen {
key name;
leaf name {
type string;
}
leaf address {
description "Local IP address to listen on for inbound SSH 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.";
type inet:ip-address;
}
leaf port {
description "Local port for SSH daemon to listen on.";
type inet:port-number;
}
}
}
container web {
description "Web services";