Inspired by this blog post and the final run cost of my Step CA on GCP project (30 AUD / month), I decided to simply run my CA on an existing rpi4.
The use of a Yubikey is not necessary, but does secure the key material in an offboard device isn't
easily accessible from within the docker container. The step-ca
process of course needs to be able
to use the keys to sign certificates etc, but a malicious user could not exfiltrate them as when
they're written to disk.
Install the C libs required for the Python install:
sudo apt install libpcsclite-dev pcscd swig python3-dev
Then install the latest via pip:
sudo pip install --user yubikey-manager
Check everything is working (as root!):
# ykman -v
YubiKey Manager (ykman) version: 5.0.1
# ykman info
WARNING: Failed opening device
Device type: YubiKey 5 Nano
Serial number: 12508380
Firmware version: 5.2.7
Form factor: Nano (USB-A)
Enabled USB interfaces: OTP, FIDO, CCID
Applications
OTP Enabled
FIDO U2F Enabled
FIDO2 Enabled
OATH Enabled
PIV Enabled
OpenPGP Enabled
YubiHSM Auth Not available
Reset the PIV settings on the Yubikey to their defaults:
# ykman piv reset
WARNING! This will delete all stored PIV data and restore factory settings. Proceed? [y/N]: y
Resetting PIV data...
Success! All PIV data have been cleared from the YubiKey.
Your YubiKey now has the default PIN, PUK and Management Key:
PIN: 123456
PUK: 12345678
Management Key: 010203040506070801020304050607080102030405060708
ykman piv access change-pin
ykman piv access change-puk
ykman piv access change-management-key --generate --protect
The following is a working example of configuring step-ca
. The generated password will be used for
all certificate keys, and also for the "admin" provisioner.
> export STEPPATH=/tmp/step && mkdir -p $STEPPATH
> step ca init --name="mafro.dev CA" --provisioner=admin --dns=certs.mafro.dev --address=':443'
✔ What do you want your password to be? [leave empty and we'll generate one]:
✔ Password: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Generating root certificate...
all done!
Generating intermediate certificate...
Generating user and host SSH certificate signing keys...
all done!
✔ Root certificate: /tmp/step/certs/root_ca.crt
✔ Root private key: /tmp/step/secrets/root_ca_key
✔ Root fingerprint: c7641ce4f91993dc3f00000000000000000000000f829c626d20fa02d89600e0
✔ Intermediate certificate: /tmp/step/certs/intermediate_ca.crt
✔ Intermediate private key: /tmp/step/secrets/intermediate_ca_key
✔ Database folder: /tmp/step/db
✔ Templates folder: /tmp/step/templates
✔ Default configuration: /tmp/step/config/defaults.json
✔ Certificate Authority configuration: /tmp/step/config/ca.json
Add both the root and intermediate into slots 82
and 83
, respectively:
> ykman piv certificates import 82 certs/root_ca.crt
Enter a management key [blank to use default key]:
> ykman piv keys import 82 secrets/root_ca_key
Enter a management key [blank to use default key]:
Enter password to decrypt key:
>
> ykman piv certificates import 83 certs/intermediate_ca.crt
Enter a management key [blank to use default key]:
> ykman piv keys import 83 root/secrets/intermediate_ca_key
Enter a management key [blank to use default key]:
Enter password to decrypt key:
The following config sets up the CA to use the Yubikey intermediate certs/keys for signing:
"key": "yubikey:slot-id=83",
"kms": {
"type": "yubikey",
"pin": "YUBIPIN"
},
All this config is committed to Github, so I certainly don't want to also include my Yubikey pin. A solution is passing via environment variables. Step CA doesn't natively support this, so a small bit of jq surgery is necessary.
Normally the --ssh
parameter to step ca init
is used to configure the CA server to be able to
generate SSH certs. In this case, we will instead use the Yubikey to generate the keypairs,
retaining the private component only on the Yubikey.
> ykman piv keys generate -a ECCP256 84 certs/ssh_host_ca_key.pub
> Enter a management key [blank to use default key]:
>
> ykman piv keys generate -a ECCP256 85 certs/ssh_user_ca_key.pub
> Enter a management key [blank to use default key]:
The following stanza is added to the CA config at $STEPPATH/config/ca.json
:
"ssh": {
"hostKey": "yubikey:slot-id=84",
"userKey": "yubikey:slot-id=85"
},
Reference: Enable SSH After Init
This section is essentially short-form instructions derived from smallstep.com/blog/diy-single-sign-on-for-ssh.
Smallstep CA can issue certs for use with SSH. By configuring Google oAuth as the identity provider,
Google does the authentication for us, and step-ca
issues the cert.
┌──────────┐ ┌──────────┐ ┌─ ── ── ── ── ─┐
│ │ │ │
│ Client │────SSH────▶│ Server │ │ Google │
│ (macOS) │ │ (locke) │ oAuth app
│ │ │ │ │ │
└──────────┘ └──────────┘
│ └─ ── ── ── ── ─┘
│ ▲
│ ┌──────────┐ │
request │ │ │
cert─────────────▶│ CA │────authenticate───┘
│ (ringil) │
│ │
└──────────┘
Note: The naming convention here is to SSH from the client into the host server.
- Configure oAuth consent at https://console.developers.google.com/apis/credentials/consent
- Create an oAuth app at https://console.cloud.google.com/apis/credentials
a. Click
Create credentials
, choosingOAuth client ID
b. SelectDesktop app
as application type c. Retain your client ID and client secret
Next, we must configure the CA with a new OIDC provisioner (named "Google") using above secrets. The
--domain
parameter is your Google SSO domain name.
> step ca provisioner add Google --type=OIDC --ssh \
--client-id "$OIDC_CLIENT_ID" \
--client-secret "$OIDC_CLIENT_SECRET" \
--configuration-endpoint 'https://accounts.google.com/.well-known/openid-configuration' \
--domain mafro.net
Success! Your `step-ca` config has been updated. To pick up the new configuration SIGHUP (kill -1 <pid>) or restart the step-ca process.
Next our CA needs to trust an identity document provided by the host system. In the blog post, the host is an AWS EC2 instance which provides its instance identity to the CA server, and is trusted via the Amazon signature of the AWS account ID (see script here).
On the host server, install the Smallstep CLI tools. Next, bootstrap the
step
client as usual:
> FINGERPRINT=$(step certificate fingerprint root_ca.crt)
> step ca bootstrap --ca-url https://ringil --fingerprint $FINGERPRINT
The root certificate has been saved in $HOME/.step/certs/root_ca.crt.
Your configuration has been saved in $HOME/.step/config/defaults.json.
Generate a certificate and configure sshd
to use it. Run the following as root, so it's possible
to write /etc/ssh
.
In the following example, the host server is named locke
. The steps are:
- Generate a token with the
admin
provisioner - Inspect the token for your amusement
> TOKEN=$(step ca token $(hostname) --ssh --host --provisioner admin)
✔ Provisioner: admin (JWK) [kid: ydABxIT07b0000000000000000000000nGYFRfEGmNA]
✔ Please enter the password to decrypt the provisioner key:
> echo $TOKEN | step crypto jwt inspect --insecure
{
"header": {
"alg": "ES256",
"kid": "ydABxIT07bl-G9jSxfCB45pxNylrKitsnGYFRfEGmNA",
"typ": "JWT"
},
"payload": {
"aud": "https://ringil:8443/1.0/ssh/sign",
"exp": 1618046362,
"iat": 1618046062,
"iss": "admin",
"jti": "776b2fce13c90b675f0a1f55712eee80f2504f5f6d4723e0a4fd80e5d35fde40",
"nbf": 1618046062,
"sha": "b07c800d7bf36422bd7da01fc2db11efebaafdd5b83092ff82136e75a6d033f9",
"step": {
"ssh": {
"certType": "host",
"keyID": "locke",
"principals": [],
"validAfter": "",
"validBefore": ""
}
},
"sub": "locke"
},
"signature": "E-b6SIaN9atMMo-ICdnoUCjQWMLYuJxkVuB5dBDGjxtzKpPyC-ydnLH5qYV9TTss7MgA2tciMNi9ka-PJ0LNqg"
}
> step ssh certificate $(hostname) /etc/ssh/ssh_host_ecdsa_key.pub --host --sign --provisioner admin --principal $(hostname) --token $TOKEN
✔ CA: https://ringil:8443
✔ Would you like to overwrite /etc/ssh/ssh_host_ecdsa_key-cert.pub [y/n]: y
✔ Certificate: /etc/ssh/ssh_host_ecdsa_key-cert.pub
> step ssh config --host --set Certificate=ssh_host_ecdsa_key-cert.pub --set Key=ssh_host_ecdsa_key
✔ /etc/ssh/sshd_config
✔ /etc/ssh/ca.pub
> systemctl restart sshd
The following steps are run on the client system, which is connecting to the host configured above.
> FINGERPRINT=$(step certificate fingerprint root_ca.crt)
> step ca bootstrap --ca-url https://ringil --fingerprint $FINGERPRINT
The root certificate has been saved in /Users/blackm/.step/certs/root_ca.crt.
Your configuration has been saved in /Users/blackm/.step/config/defaults.json.
> step ssh config
✔ /Users/mafro/.ssh/config
✔ /Users/mafro/.step/ssh/config
✔ /Users/mafro/.step/ssh/known_hosts
Configure your SSH client config such that step is used to generate the SSH certificate on demand:
> cat ~/.ssh/config
Host locke
User pi
UserKnownHostsFile /Users/blackm/.step/ssh/known_hosts
ProxyCommand step ssh proxycommand %r %h %p --provisioner Google
The Google
provisioner is the OIDC one created at the beginning.
Now, using this configuration is as simple as ssh locke
, and the OIDC flow is triggered:
> ssh locke
✔ Provisioner: Google (OIDC) [client: 824164598483-frmggjqidnm16kjob9ud8a6a6ahvub1v.apps.googleusercontent.com]
Your default web browser has been opened to visit:
https://accounts.google.com/o/oauth2/v2/auth?<snip>
✔ CA: https://ringil:8443
Linux locke 5.10.17-v7l+ #1414 SMP Fri Apr 30 13:20:47 BST 2021 armv7l
The programs included with the Debian GNU/Linux system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.
Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
permitted by applicable law.
Last login: Thu Jun 17 06:07:51 2021 from 192.168.1.139
pi@locke:~ >
If you wanted to have a peek at your SSH certificate, as provisioned by your CA:
> step ssh list --raw | step ssh inspect
-:
Type: [email protected] user certificate
Public key: ECDSA-CERT SHA256:1p9Ux0LVclOe3wFH9ISo+eUiqoAi/CoK7bE/VSdf2r0
Signing CA: ECDSA SHA256:WoobT5Uoi8cddLhcxILd5eLoPiq27iEaVCDV/oL/B6I
Key ID: "[email protected]"
Serial: 8826815887645788865
Valid: from 2021-06-17T05:44:17 to 2021-06-17T21:44:17
Principals:
m
[email protected]
mafro
pi
Critical Options: (none)
Extensions:
permit-agent-forwarding
permit-port-forwarding
permit-pty
permit-user-rc
permit-X11-forwarding
- https://smallstep.com/blog/diy-single-sign-on-for-ssh/
- https://github.com/smallstep/certificates/blob/master/docs/provisioners.md#oidc
When using the OIDC provisioner
to issue SSH certs, you are limited to only issuing certs with a principal which matches the email
of the OIDC identity - eg. if your email is [email protected]
, then the principals on your cert will
be bob
and [email protected]
.
This is fine if you're logging into a server as bob
, using an OIDC identity of [email protected]
.
It doesn't work if you're, say, logging in as user pi
, using an OIDC identity of [email protected]
.
This can be solved using templated SSH certs!
Modify the principals
field of an SSH user template, and
update the CA config at $STEPPATH/config/ca.json
to include the following to the OIDC
provisioner:
"options": {
"ssh": {
"templateFile": "templates/ssh/mafro.tpl"
}
}
Smallstep doesn't distribute a binary for Raspberry Pi Zero armv6 architecture. Use the following
commands to build on macOS. You could build on Raspbian, but the golang version in apt was 1.11, and
too old to build step
at time of writing.
git clone --branch=v0.15.14 https://github.com/smallstep/cli.git /tmp/step-cli
cd /tmp/step-cli
GOOS=linux GOARCH=arm GOARM=6 make build
tar czf step-0.15.14-armv6.tar.gz -C bin step
mv step-0.15.14-armv6.tar.gz ~