Skip to content

Instantly share code, notes, and snippets.

@tashian
Last active July 15, 2024 18:56
Show Gist options
  • Save tashian/fde43668cbf6e3227fb13ef51db650b8 to your computer and use it in GitHub Desktop.
Save tashian/fde43668cbf6e3227fb13ef51db650b8 to your computer and use it in GitHub Desktop.
#!/bin/bash
#
# This script will get an SSH host certificate from our CA and add a weekly
# cron job to rotate the host certificate. It should be run as root.
#
# See https://smallstep.com/blog/diy-single-sign-on-for-ssh/ for full instructions
CA_URL="[Your CA URL]"
# Obtain your CA fingerprint by running this on your CA:
# # step certificate fingerprint $(step path)/certs/root_ca.crt
CA_FINGERPRINT="[Your CA Fingerprint]"
case $(arch) in
x86_64)
ARCH="amd64"
;;
aarch64)
ARCH="arm64"
;;
esac
# Install step
STEP_VERSION=$(curl -s https://api.github.com/repos/smallstep/cli/releases/latest | jq -r '.tag_name')
curl -sLO https://github.com/smallstep/cli/releases/download/${STEP_VERSION}/step-cli_${STEP_VERSION:1}_${ARCH}.deb
dpkg -i step-cli_${STEP_VERSION:1}_${ARCH}.deb
# Configure `step` to connect to & trust our `step-ca`.
# Pull down the CA's root certificate so we can talk to it later with TLS
step ca bootstrap --ca-url $CA_URL \
--fingerprint $CA_FINGERPRINT
# Install the CA cert for validating user certificates (from /etc/step-ca/certs/ssh_user_key.pub` on the CA).
step ssh config --roots > $(step path)/certs/ssh_user_key.pub
# Get AWS host metadata (IMDSv2)
AWS_TOKEN=$(curl -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600")
LOCAL_HOSTNAME=$(curl -H "X-aws-ec2-metadata-token: $AWS_TOKEN" -s http://169.254.169.254/latest/meta-data/local-hostname)
LOCAL_IP=$(curl -H "X-aws-ec2-metadata-token: $AWS_TOKEN" -s http://169.254.169.254/latest/meta-data/local-ipv4)
PUBLIC_HOSTNAME=$(curl -H "X-aws-ec2-metadata-token: $AWS_TOKEN" -s http://169.254.169.254/latest/meta-data/public-hostname)
PUBLIC_IP=$(curl -H "X-aws-ec2-metadata-token: $AWS_TOKEN" -s http://169.254.169.254/latest/meta-data/public-ipv4)
AWS_ACCOUNT_ID=$(curl -H "X-aws-ec2-metadata-token: $AWS_TOKEN" -s http://169.254.169.254/latest/dynamic/instance-identity/document | grep accountId | awk '{print $3}' | sed 's/"//g' | sed 's/,//g')
# Get an SSH host certificate
# This helps us avoid a potential race condition / clock skew issue
# "x509: certificate has expired or is not yet valid: current time 2020-04-01T17:52:51Z is before 2020-04-01T17:52:52Z"
sleep 1
# The TOKEN is a JWT with the instance identity document and signature embedded in it.
TOKEN=$(step ca token $PUBLIC_HOSTNAME --ssh --host --provisioner "Amazon Web Services")
# To inspect $TOKEN, run
# $ echo $TOKEN | step crypto jwt inspect --insecure
#
# To inspect the Instance Identity Document embedded in the token, run
# $ echo $TOKEN | step crypto jwt inspect --insecure | jq -r ".payload.amazon.document" | base64 -d
# Ask the CA to exchange our instance token for an SSH host certificate
step ssh certificate $PUBLIC_HOSTNAME /etc/ssh/ssh_host_ecdsa_key.pub \
--host --sign --provisioner "Amazon Web Services" \
--principal $PUBLIC_HOSTNAME --principal $LOCAL_HOSTNAME \
--token $TOKEN
# Configure and restart `sshd`
tee -a /etc/ssh/sshd_config > /dev/null <<EOF
# SSH CA Configuration
# This is the CA's public key, for authenticatin user certificates:
TrustedUserCAKeys $(step path)/certs/ssh_user_key.pub
# This is our host private key and certificate:
HostKey /etc/ssh/ssh_host_ecdsa_key
HostCertificate /etc/ssh/ssh_host_ecdsa_key-cert.pub
EOF
service ssh restart
# Now add a weekly cron script to rotate our host certificate.
cat <<EOF > /etc/cron.weekly/rotate-ssh-certificate
#!/bin/sh
export STEPPATH=/root/.step
cd /etc/ssh && step ssh renew ssh_host_ecdsa_key-cert.pub ssh_host_ecdsa_key --force 2> /dev/null
exit 0
EOF
chmod 755 /etc/cron.weekly/rotate-ssh-certificate
@tashian
Copy link
Author

tashian commented Feb 26, 2024

Thanks @bdollerup, I've updated the script to detect arch, and to use the latest version of step-cli

@bdollerup
Copy link

@tashian Cool! My pleasure. I love what you did with the version, but why not just use "https://dl.smallstep.com/cli/docs-ca-install/latest/step-cli_${MACHINE_ARCH}.deb"?

@avoidik
Copy link

avoidik commented Mar 9, 2024

be careful with this cron task on amazon linux 2023, because cronie (cron.d) is not installed by default, this needs to be a systemd timer or you need to install it via dnf install cronie -y

@tashian
Copy link
Author

tashian commented Mar 12, 2024

good call @avoidik

@minecraftchest1
Copy link

minecraftchest1 commented Jul 2, 2024

Why is this making http requests tona random ip?

HOSTNAME="$(curl -s http://169.254.169.254/latest/meta-data/public-hostname)"
LOCAL_HOSTNAME="$(curl -s http://169.254.169.254/latest/meta-data/local-hostname)"

There are certainly better ways to get the public DNS name, and no reason not to use hostnamectl hostname or hostname to get the local hostname.

EDIT: Did some digging and apparantly those are AWS metadata servers is a link-local ip block. I was wrong to think they were anything sketchy.

@tashian
Copy link
Author

tashian commented Jul 2, 2024

@minecraftchest1 This script is for AWS EC2 hosts. On AWS, the instance metadata API has the public and local hostnames of the instance. The local hostname is the FQDN of the internal hostname for the instance (eg. ip-172-31-18-226.us-east-2.compute.internal), whereas hostname would return just ip-172-31-18-226. For the purpose of minting an SSH host certificate, I wanted public and local FQDNs.

@minecraftchest1
Copy link

@minecraftchest1 This script is for AWS EC2 hosts. On AWS, the instance metadata API has the public and local hostnames of the instance. The local hostname is the FQDN of the internal hostname for the instance (eg. ip-172-31-18-226.us-east-2.compute.internal), whereas hostname would return just ip-172-31-18-226. For the purpose of minting an SSH host certificate, I wanted public and local FQDNs.

I realized that right after I posted that comment, hense the edit. Thanks for the clarification and additional info anyhow.

@tvpartner
Copy link

for the idmsv2 update just update the following

TOKEN=$(curl -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600")

LOCAL_HOSTNAME=$(curl -H "X-aws-ec2-metadata-token: $TOKEN" -s http://169.254.169.254/latest/meta-data/local-hostname)
LOCAL_IP=$(curl -H "X-aws-ec2-metadata-token: $TOKEN" -s http://169.254.169.254/latest/meta-data/local-ipv4)
PUBLIC_HOSTNAME=$(curl -H "X-aws-ec2-metadata-token: $TOKEN" -s http://169.254.169.254/latest/meta-data/public-hostname)
PUBLIC_IP=$(curl -H "X-aws-ec2-metadata-token: $TOKEN" -s http://169.254.169.254/latest/meta-data/public-ipv4)
AWS_ACCOUNT_ID=$(curl -H "X-aws-ec2-metadata-token: $TOKEN" -s http://169.254.169.254/latest/dynamic/instance-identity/document | grep accountId | awk '{print $3}' | sed 's/"//g' | sed 's/,//g')

@tashian
Copy link
Author

tashian commented Jul 15, 2024

Thanks @tvpartner, I've just updated the script to use this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment