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_deckhouse method).
  • 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.

  1. Enable the SSH secrets engine:

    d8 stronghold secrets enable -path=ssh-client-signer ssh
  2. Generate 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.

  1. Save the CA public key on each server. The public_key endpoint does not require authentication:

    curl -o /etc/ssh/trusted-user-ca-keys.pem https://stronghold.example.com/v1/ssh-client-signer/public_key
  2. Add the following to /etc/ssh/sshd_config:

    TrustedUserCAKeys /etc/ssh/trusted-user-ca-keys.pem
  3. Restart 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.

  1. Get the auth method accessor:

    OIDC_ACCESSOR=$(d8 stronghold read -field=accessor sys/auth/oidc_deckhouse)
  2. 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 the oidc_deckhouse method;
    • <SSH_USER>: the user’s account name on the servers.

    The metadata parameter replaces all entity metadata. If the entity already has metadata, pass all keys in the same command.

  3. 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"
    }
    EOF

    If 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 with user_claim set to preferred_username.

Step 4. Grant signing permissions

Allow members of an IdP group to sign keys with the role.

  1. Create a policy:

    d8 stronghold policy write ssh-ops - <<'POLICY'
    path "ssh-client-signer/sign/ops" {
      capabilities = ["update"]
    }
    POLICY
  2. Bind 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=ops in group-alias is 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.

  1. Log in to Stronghold:

    d8 stronghold login -method=oidc -path=oidc_deckhouse
  2. If you do not have an SSH key, create one:

    ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519
  3. Sign 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.pub
  4. Connect to the server. OpenSSH uses the *-cert.pub file 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:

  1. View the certificate contents:

    ssh-keygen -Lf ~/.ssh/id_ed25519-cert.pub

    The Principals field must contain your account name on the servers, and the Valid field must show an interval of no more than 30 minutes.

  2. 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=root
  3. After 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.

  1. Delete the external group and its alias: d8 stronghold delete identity/group/name/ops.

  2. Delete the policy and the role:

    d8 stronghold policy delete ssh-ops
    d8 stronghold delete ssh-client-signer/roles/ops
  3. For a test setup, disable the mount: d8 stronghold secrets disable ssh-client-signer. Then remove the TrustedUserCAKeys line on the servers.

Additional resources