Instead of distributing public keys to authorized_keys on every server, servers trust a single CA in Stronghold. A user logs in to Stronghold through OIDC, signs their public key, and gets a certificate valid for 30 minutes. You do not need to revoke access on servers: the certificate expires on its own.
Goal
Set up an SSH CA in Stronghold, a role that lets users log in only under their own name, CA trust on servers, and certificate issuance after an OIDC login.
Prerequisites
Before you start, prepare the following:
- A Stronghold token with permissions to configure secrets engines, policies, and Identity.
- A configured OIDC auth method that passes user groups (in DP, the
oidc_deckhousemethod). - Servers with OpenSSH where you can change
sshd_config. - The account name of each user on the servers. The example stores it in entity metadata (step 3).
Step 1. Set up the SSH CA
Enable the SSH secrets engine and generate a CA key.
Enable the SSH secrets engine:
d8 stronghold secrets enable -path=ssh-client-signer sshGenerate the CA key:
d8 stronghold write ssh-client-signer/config/ca generate_signing_key=true
Step 2. Configure trust on servers
On each server, configure sshd to trust certificates signed by the Stronghold CA.
Save the CA public key on each server. The
public_keyendpoint does not require authentication:curl -o /etc/ssh/trusted-user-ca-keys.pem https://stronghold.example.com/v1/ssh-client-signer/public_keyAdd the following to
/etc/ssh/sshd_config:TrustedUserCAKeys /etc/ssh/trusted-user-ca-keys.pemRestart
sshd:sudo systemctl restart sshd
Distribute the key and the setting with a configuration management system (Ansible, Puppet, Salt).
Step 3. Create a role with a templated user name
The role allows signing a key only for the principal that matches the user’s account name on the servers. The account name is substituted by an Identity template.
For the oidc_deckhouse method in DP, the entity alias name is the user’s email: the module sets user_claim to email in the deckhouse_dex_authenticated role. An email does not match an account name on a server, so the example stores the account name in the ssh_user key of the entity metadata. Only a token with permissions on identity/entity/* can change entity metadata, so users cannot change their own principal.
Get the auth method accessor:
OIDC_ACCESSOR=$(d8 stronghold read -field=accessor sys/auth/oidc_deckhouse)Set the server account name for each user. The user entity is created at the first login to Stronghold:
ENTITY_ID=$(d8 stronghold write -field=id identity/lookup/entity alias_name="<USER_EMAIL>" alias_mount_accessor="$OIDC_ACCESSOR") d8 stronghold write identity/entity/id/$ENTITY_ID metadata=ssh_user=<SSH_USER>Placeholders:
<USER_EMAIL>: the user’s email, which is the alias name on theoidc_deckhousemethod;<SSH_USER>: the user’s account name on the servers.
The
metadataparameter replaces all entity metadata. If the entity already has metadata, pass all keys in the same command.Create the role:
d8 stronghold write ssh-client-signer/roles/ops - <<EOF { "key_type": "ca", "algorithm_signer": "rsa-sha2-256", "allow_user_certificates": true, "allowed_users_template": true, "allowed_users": "{{identity.entity.metadata.ssh_user}}", "default_user_template": true, "default_user": "{{identity.entity.metadata.ssh_user}}", "allowed_extensions": "permit-pty,permit-port-forwarding", "default_extensions": { "permit-pty": "" }, "ttl": "30m", "max_ttl": "1h" } EOFIf the alias name already matches the server account name, use the
{{identity.entity.aliases.<OIDC_ACCESSOR>.name}}template instead of metadata, where<OIDC_ACCESSOR>is the method accessor. For example, this applies to a separate OIDC mount withuser_claimset topreferred_username.
Step 4. Grant signing permissions
Allow members of an IdP group to sign keys with the role.
Create a policy:
d8 stronghold policy write ssh-ops - <<'POLICY' path "ssh-client-signer/sign/ops" { capabilities = ["update"] } POLICYBind the policy to an IdP group through an external Identity group:
GROUP_ID=$(d8 stronghold write -field=id identity/group name=ops type=external policies=ssh-ops) d8 stronghold write identity/group-alias name=ops mount_accessor="$OIDC_ACCESSOR" canonical_id="$GROUP_ID"Here,
name=opsingroup-aliasis the group name that the IdP passes in the groups claim.
Step 5. Get a certificate and log in to a server
The user performs these actions on their workstation.
Log in to Stronghold:
d8 stronghold login -method=oidc -path=oidc_deckhouseIf you do not have an SSH key, create one:
ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519Sign the public key and save the certificate next to the key:
d8 stronghold write -field=signed_key ssh-client-signer/sign/ops public_key=@$HOME/.ssh/id_ed25519.pub > ~/.ssh/id_ed25519-cert.pubConnect to the server. OpenSSH uses the
*-cert.pubfile automatically:ssh <SSH_USER>@server.example.com<SSH_USER>is your account name on the servers from step 3.
Repeat steps 1 and 3 after the certificate expires.
Verification
Check the result:
View the certificate contents:
ssh-keygen -Lf ~/.ssh/id_ed25519-cert.pubThe
Principalsfield must contain your account name on the servers, and theValidfield must show an interval of no more than 30 minutes.Try to sign a key for another principal; the request must be rejected:
d8 stronghold write ssh-client-signer/sign/ops public_key=@$HOME/.ssh/id_ed25519.pub valid_principals=rootAfter 30 minutes, make sure the server rejects login with the same certificate.
For login errors, see Troubleshooting.
Cleanup
Delete the objects created in this guide.
Delete the external group and its alias:
d8 stronghold delete identity/group/name/ops.Delete the policy and the role:
d8 stronghold policy delete ssh-ops d8 stronghold delete ssh-client-signer/roles/opsFor a test setup, disable the mount:
d8 stronghold secrets disable ssh-client-signer. Then remove theTrustedUserCAKeysline on the servers.