SSH certificates are a very powerful tool . Initially, in the certification center,
step-ca
we implemented only a minimal set of functions for authentication using user and host certificates. Then we added X.509 certificate templates , and in August last year - and SSH templates, in version 0.15.2. Finally, we have documented this feature and are ready to talk about it.
The templates for SSH certificates work in a similar way to the X.509 templates: they are JSON files written in Go
text/template
. They are used to configure the SSH certificates that it issues
step-ca
. Let's take a look at what these templates are and how you can use them.
By default, a custom SSH certificate template looks like this:
{
"type": {{ toJson .Type }},
"keyId": {{ toJson .KeyID }},
"principals": {{ toJson .Principals }},
"extensions": {{ toJson .Extensions }},
"criticalOptions": {{ toJson .CriticalOptions }}
}
And here is the SSH certificate issued using this template:
$ step ssh inspect id_ct-cert.pub
id_ct-cert.pub:
Type: ecdsa-sha2-nistp256-cert-v01@openssh.com user certificate
Public key: ECDSA-CERT SHA256:iczSh1XiBBE36yfJcDidgp6fqY3qWx1RtEwFfAN9jDs
Signing CA: ECDSA SHA256:MKwRQ/SDKk/pCJbbCk5bfhZACjSjv7uZXLyc5n4Wx6k
Key ID: "carl@smallstep.com"
Serial: 2831574724231262409
Valid: from 2020-11-17T16:48:11 to 2020-11-18T08:49:11
Principals:
carl
carl@smallstep.com
Critical Options: (none)
Extensions:
permit-X11-forwarding
permit-agent-forwarding
permit-port-forwarding
permit-pty
permit-user-rc
It allows the user
carl
(or
carl@smallstep.com
) to authenticate to any SSH host that trusts my SSH CA. The certificate includes some basic extensions:
permit-x11-forwarding
: Enables X11 forwarding (withssh -X
) to run remote X11 programs on the local display.
permit-agent-forwarding
: Allows agent redirection (usingssh -A
) to forward keys from the local SSH agent to the remote host (see details on the SSH agent here ).
permit-port-forwarding
: Allows port forwarding (tunneling) from local to remote port (ssh -L
) or remote to local (ssh -R
).
permit-pty
: A very important extension. If you want to open an interactive session in the console, the host must allocate a pty (pseudo-tty) for you. Otherwise, no interactivity is provided. For example, to test SSH authentication on GitHub one can runssh -T git@github.com
(-T
disables pty request).
permit-user-rc
: Run a personal RC file after connecting (located~/.ssh/rc
on the remote host).
User and host certificates support extensions and critical parameters, but OpenSSH does not define any built-in extensions or critical parameters for host certificates. Thus, all the most interesting happens with user certificates, so in this article we will consider only them.
Examples of certificate templates
Let's make a few changes to the default template.
Prohibiting agent and port forwarding
If users are connecting to internal hosts through the bastion host , it would be good to disable port forwarding for security reasons. You don't want users to redirect traffic from the MySQL production server to their localhost. Similarly, agent forwarding carries a security risk . Here's a template that simply removes these two extensions from SSH certificates:{
"type": {{ toJson .Type }},
"keyId": {{ toJson .KeyID }},
"principals": {{ toJson .Principals }},
"extensions": {
"permit-x11-forwarding": "",
"permit-pty": "",
"permit-user-rc": ""
},
"criticalOptions": {{ toJson .CriticalOptions }}
}
Embedding the force-command directive
ForceCommand
Is a server-side SSHD configuration directive that runs an alternate command on the host instead of an interactive terminal. But with the same effect, you can embed it
force-command
directly into the certificate - into the section
Critical Options:
. It can be useful for service accounts that need to execute only one command, for example, start a task on a remote system.
Restricting connections by addresses
To restrict the scope of a certificate, a list of allowed IP addresses (CIDR blocks) can be embedded in it.Here is a certificate template that uses both
source-address
, and
force-command
.
{
"type": {{ toJson .Type }},
"keyId": {{ toJson .KeyID }},
"principals": {{ toJson .Principals }},
"extensions": {{ toJson .Extensions }},
"criticalOptions": {
"force-command": "echo \"Hello World\"",
"source-address": "10.20.30.0/24,1.1.1.1/32"
}
}
This is a normal example, but here the IP list is rigidly fixed and does not change. And we usually want to use different address lists for different users. Let's try…
Insert different values for different users
Obviously, users cannot be given the right to edit the address range. Therefore, dynamic values must come from a reliable source.To do this,
step-ca
through the OpenID Connect (OIDC) provider, you can configure the OAuth provider to add custom claims to the token containing CIRD address blocks that we want to add to this user's certificate.
The OIDC provider is the perfect way to issue SSH certificates to step-ca. In the DIY Single Sign-On for SSH article, I covered how to configure an SSH CA to issue short-term SSH certificates using ID tokens from a trusted OAuth provider. If a
step-ca
configured as an OAuth trusted client, it will read the field
email
from the ID token and retrieve the list of SSH certificate principals from there (for example,
carl@smallstep.com
certificates for
carl
and will be generated by the field
carl@smallstep.com
).
But OIDC allows reading ID and other fields from tokens through templates . This is where the real magic begins. Thus, we add a separate field to the user directory on the side of the identity provider
source_address
- and reflect it in our ID token. Then, through the SSH template, you can enter the value from the token into the certificate. Here's the template:
{
"type": {{ toJson .Type }},
"keyId": {{ toJson .KeyID }},
"principals": {{ toJson .Principals }},
"extensions": {{ toJson .Extensions }},
{{ if .Token.source_address }}
"criticalOptions": {
"source-address": "{{ .Token.source_address }}"
}
{{ else }}
"criticalOptions": {{ toJson .CriticalOptions }}
{{ end }}
}
GitHub authentication by certificate
Let's look at another example of a custom claim. Using GitHub Enterprise Cloud or GitHub Enterprise Server, you can configure GitHub to use SSH certificates. Specifically, GitHub will trust your SSH certification authority . But for everything to work, you need to create a separate SSH certificate for each user with an extensionlogin@github.com
that specifies the username on GitHub. With this extension, the certificate authenticates the user to GitHub Enterprise. And it's great: the same certificate allows you to both connect to your server via SSH and push the code to GitHub.
Here is a certificate template with custom GitHub extension support:
{
"type": {{ toJson .Type }},
"keyId": {{ toJson .KeyID }},
"principals": {{ toJson .Principals }},
"criticalOptions": {{ toJson .CriticalOptions }},
{{ if .Token.ghu }}
"extensions": {
"login@github.com": {{ toJson .Token.ghu }}
}
{{ else }}
"extensions": {{ toJson .Extensions }}
{{ end }}
}
To use the template, you need to add an individual requirement
ghu
(“GitHub Username”) to the OIDC identification tokens . Let's take a closer look at how to create this custom claim using your OAuth provider.
Registering an application with an identity provider
Not all identity providers support individual requirements, but if they do, the process is pretty similar. Here's how it's done with Okta:- Add the OAuth app to Okta and trust it with the OIDC provider in
step-ca
as described in the DIY SSO for SSH article .
- Add a new field to your Okta user directory (for example
GitHub Username
)
- Add an individual requirement to the OIDC token , for example, with a short name
ghu
- Now we fill in the field for the test user and check the requirement. Okta has an ID token testing tool. Or can be used
step
to validate the entire OAuth flow:
OIDC_ENDPOINT="https://[your organization].okta.com/oauth2/default/.well-known/openid-configuration" CLIENT_ID="[your OAuth client ID]" CLIENT_SECRET="[your OAuth client secret]" step oauth --oidc --provider $OIDC_ENDPOINT \ --client-id $CLIENT_ID --client-secret $CLIENT_SECRET \ --listen=":10000" --bare | step crypto jwt inspect --insecure
- Finally,
step-ca
to use this pattern. The provider config should reference the template file:
{ "provisioners": [ { "type": "OIDC", "name": "Okta", "clientID": "[your OAuth client ID]", "clientSecret": "[your OAuth client secret]", "configurationEndpoint": "https://[your organization].okta.com/oauth2/default/.well-known/openid-configuration", "listenAddress": ":10000", "options": { "ssh": { "templateFile": "templates/certs/ssh/github.tpl" } } }, ... ] }
What's next
We've added a section on SSH Templates to the documentation that goes into more detail on all parameters and variables.
If you have any questions, do not hesitate to ask .