Examples of smart use of SSH templates





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 (with ssh -X



    ) to run remote X11 programs on the local display.

  • permit-agent-forwarding



    : Allows agent redirection (using ssh -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 run ssh -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 extension login@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:



  1. 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 .





  2. Add a new field to your Okta user directory (for example GitHub Username



    )

  3. Add an individual requirement to the OIDC token , for example, with a short nameghu





  4. 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
          
          





  5. 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 .



All Articles