From 88f165363d48cb02e929020d1f62dbb1f0aa093b Mon Sep 17 00:00:00 2001 From: "Dustin C. Hatch" Date: Tue, 3 Oct 2023 19:17:29 -0500 Subject: [PATCH] step-ssh: Automatically issue/renew SSH host certs The `ssh-bootstrap` script, which is run by the *ssh-bootstrap.service* systemd unit, requests SSH host certificates for each of the existing SSH host keys. The certificates are issued by the *POST /sshkeys/sign* operation of *dch-webhooks* web service. The *step-ssh-renew* timer/service runs `step ssh renew`, in a container, on a weekly basis to renew the SSH host certificate. A host certificate must already exist, and its private key is used to authenticate to the CA server. Since `step ssh renew` can only operate on one certificate/key file at a time, the `step-ssh-renew@.container` defines a template unit. The template instance specifies the key type (i.e. `rsa`, `ecdsa`, or `ed25519`), which in turn defines which certificate and private key file to use. The timer unit activates a target unit, which depends on the concrete service units. Note that the target unit must have `StopWhenUnneeded=yes` so that it can be restarted again the next time the timer fires. --- ssh-bootstrap.service | 13 +++++++++++++ ssh-bootstrap.sh | 35 +++++++++++++++++++++++++++++++++++ step-ssh-renew.env | 3 +++ step-ssh-renew.target | 6 ++++++ step-ssh-renew.timer | 11 +++++++++++ step-ssh-renew@.container | 20 ++++++++++++++++++++ step-ssh.yaml | 29 +++++++++++++++++++++++++++++ 7 files changed, 117 insertions(+) create mode 100644 ssh-bootstrap.service create mode 100644 ssh-bootstrap.sh create mode 100644 step-ssh-renew.env create mode 100644 step-ssh-renew.target create mode 100644 step-ssh-renew.timer create mode 100644 step-ssh-renew@.container create mode 100644 step-ssh.yaml diff --git a/ssh-bootstrap.service b/ssh-bootstrap.service new file mode 100644 index 0000000..9833057 --- /dev/null +++ b/ssh-bootstrap.service @@ -0,0 +1,13 @@ +# vim: set ft=systemd : +[Service] +Description=Bootstrap SSH host certificates +ConditionPathExistsGlob=!/etc/ssh/ssh_host_*_key-cert.pub +After=network-online.target +Wants=network-online.target + +[Service] +Type=oneshot +ExecStart=/bin/sh /etc/ssh/bootstrap.sh + +[Install] +WantedBy=multi-user.target diff --git a/ssh-bootstrap.sh b/ssh-bootstrap.sh new file mode 100644 index 0000000..1a8d891 --- /dev/null +++ b/ssh-bootstrap.sh @@ -0,0 +1,35 @@ +#!/bin/sh +# vim: set sw=4 ts=4 sts=4 et : + +gen_sshd_config() { + { + for x in ssh_host_*_key-cert.pub; do + printf 'HostCertificate /etc/ssh/%s\n' "${x}" + done + } > sshd_config.d/10-hostcertificate.conf +} + +parse_response() { + jq -r '.certificates | to_entries | .[] | .key + " " + .value' \ + | while read filename contents; do + [ -n "${filename}" ] || continue + echo "${contents}" > "${filename}" || return + done +} + +request_sign() { + set -- \ + https://bootstrap.pyrocufflink.blue/sshkeys/sign \ + -H 'Accept: application/json' \ + -F hostname=$(hostname -f) + for f in /etc/ssh/ssh_host_*_key.pub; do + set -- "$@" -F keys=@"${f}" + done + curl -fsSL "$@" +} + +cd /etc/ssh || exit +request_sign | parse_response +gen_sshd_config + +systemctl reload sshd diff --git a/step-ssh-renew.env b/step-ssh-renew.env new file mode 100644 index 0000000..fe30f16 --- /dev/null +++ b/step-ssh-renew.env @@ -0,0 +1,3 @@ +STEP_CA_URL=https://ca.pyrocufflink.blue:32599 +STEP_ROOT=/etc/pki/ca-trust/source/anchors/dch-root-ca.crt +STEP_PROVISIONER=sshpop diff --git a/step-ssh-renew.target b/step-ssh-renew.target new file mode 100644 index 0000000..7e7b2a1 --- /dev/null +++ b/step-ssh-renew.target @@ -0,0 +1,6 @@ +[Unit] +Description=Renew SSH host certificates +StopWhenUnneeded=yes +Wants=step-ssh-renew@ed25519.service +Wants=step-ssh-renew@ecdsa.service +Wants=step-ssh-renew@rsa.service diff --git a/step-ssh-renew.timer b/step-ssh-renew.timer new file mode 100644 index 0000000..f7344a6 --- /dev/null +++ b/step-ssh-renew.timer @@ -0,0 +1,11 @@ +[Unit] +Description=Periodically renew SSH host certificates + +[Timer] +Unit=%N.target +OnCalendar=Tue *-*-* 00:00:00 +RandomizedDelaySec=48h +Persistent=yes + +[Install] +WantedBy=timers.target diff --git a/step-ssh-renew@.container b/step-ssh-renew@.container new file mode 100644 index 0000000..117576b --- /dev/null +++ b/step-ssh-renew@.container @@ -0,0 +1,20 @@ +[Unit] +Description=Renew SSH host %I certificate +After=network-online.target +Wants=network-online.target +ConditionPathExists=/etc/ssh/ssh_host_%I_key-cert.pub + +[Container] +ContainerName=step-ssh-renew-%I +Image=docker.io/smallstep/step-cli:0.25.0 +EnvironmentFile=/etc/sysconfig/step-ssh-renew +Exec=step ssh renew -f /etc/ssh/ssh_host_%I_key-cert.pub /etc/ssh/ssh_host_%I_key +Volume=/etc/ssh:/etc/ssh:rw +Volume=/etc/pki:/etc/pki:ro +# Required in order to be able to write to /etc/ssh +SecurityLabelDisable=true +User=0 +Group=0 + +[Service] +Type=oneshot diff --git a/step-ssh.yaml b/step-ssh.yaml new file mode 100644 index 0000000..6856ba2 --- /dev/null +++ b/step-ssh.yaml @@ -0,0 +1,29 @@ +variant: fcos +version: 1.4.0 + +storage: + files: + - path: /etc/ssh/bootstrap.sh + mode: 0755 + contents: + local: ssh-bootstrap.sh + - path: /etc/containers/systemd/step-ssh-renew@.container + mode: 0644 + contents: + local: step-ssh-renew@.container + - path: /etc/sysconfig/step-ssh-renew + mode: 0600 + contents: + local: step-ssh-renew.env + - path: /etc/systemd/system/ssh-bootstrap.service + mode: 0644 + contents: + local: ssh-bootstrap.service + - path: /etc/systemd/system/step-ssh-renew.target + mode: 0644 + contents: + local: step-ssh-renew.target + - path: /etc/systemd/system/step-ssh-renew.timer + mode: 0644 + contents: + local: step-ssh-renew.timer