Configuring step-ca
Templates
People use private CAs for all sorts of things, in many different contexts:
web apps, mobile apps, code signing, cloud VM instances, SSH, IoT devices, etc.
So step-ca
must be flexible enough to handle a wide variety of flows.
X.509 and SSH certificate templates open up these possibilities. With certificate templates, you can do things like:
- Add extensions (OIDs) to X.509 certificates
- Make longer certificate chains, with multiple intermediate CAs
- Use SSH
force-command
orsource-address
extensions - Add conditionals around a certificate's parameters, and fail if they are not met
Template Syntax Overview
Certificate templates are JSON documents
that describe the most important properties in the final certificate or certificate request.
They are Go text/template
files.
Sprig functions are available inside the templates.
A few custom functions for ASN.1 encoding and time formatting are also available.
There are default X.509
and SSH
templates hardcoded into step-ca
.
On This Page
- X.509 Templates
- ASN.1 Values
- SSH Templates
- Time formatting
- Configuring
step-ca
to Use Templates - Basic X.509 Template Examples
- Advanced X.509 Template Examples
- SSH Template Examples
- Even More Templates
X.509 Templates
X.509 templates can be used in two places:
- In a CLI CA, via
step certificate create
orstep certificate sign
. - In a
step-ca
server configuration.
Here's what the default X.509 leaf certificate template looks like:
{
"subject": {{ toJson .Subject }},
"sans": {{ toJson .SANs }},
{{- if typeIs "*rsa.PublicKey" .Insecure.CR.PublicKey }}
"keyUsage": ["keyEncipherment", "digitalSignature"],
{{- else }}
"keyUsage": ["digitalSignature"],
{{- end }}
"extKeyUsage": ["serverAuth", "clientAuth"]
}
X.509 Template Fields
The fields in an X.509 template map to X.509 v3 certificate fields. See the complete list of fields supported in step-ca
templates.
X.509 Template Variables
Here are some common variables available in X.509 certificate templates:
-
.Subject: This is the subject that was passed in to
step certificate
orstep ca certificate
. Specifically,.Subject.CommonName
contains the Common Name for the certificate. -
.SANs: Subject Alternative Names. This is a list of maps containing SANs for the certificate. Unless SANs are specified (using the
--san
flag, for example), the.Subject.CommonName
is the default SAN. -
.Token: If a signed token was used to obtain the certificate —for example, with the JWK provisioner— this property contains the payload of that token.
-
.AuthorizationCrt: For provisioners that use certificates to authorize requests (eg. the X5C provisioner), this is the certificate used to authorize the current certificate request with the CA. The client has proven posession of this certificate's private key. For an X.509 authorization certificate, the
.AuthorizationCrt
is acrypto/x509
Certificate. -
.AuthorizationChain: For provisioners that use certificates to authorize requests (eg. the X5C provisioner), this is an array of the certificate chain from the request. This chain connects the authorization certificate to the root CA configured in the provisioner.
-
.Insecure.CR*: ☠️ This holds the Certificate Request (CSR) received from the client.
.Insecure.CR
is acrypto/x509
CertificateRequest. Howstep-ca
handles a Certificate Request depends on the provisioner. While a CSR is always passed to step-ca (often alongside a signed token), in most cases only the public key value in the CSR is used by step-ca. These properties are marked insecure because CSRs are not explicitly authenticated by step-ca. Instead, step-ca will authenticate the signed values from OIDC tokens, JWK tokens, cloud IIDs, and the validated SANs from ACME orders. For ACME, step-ca validates that the CSR's subject and SANs only contain values that are in the validated ACME order. -
.Insecure.CR.DNSNames: The DNS Name Subject Alternative Names (SANs) provided in the CSR
-
.Insecure.CR.IPAddresses: The IP Address SANs provided in the CSR
-
.Insecure.CR.URIs: The URI SANs provided in the CSR
-
.Insecure.CR.EmailAddresses: The Email Address SANs provided in the CSR
-
.Insecure.CR.Subject: The CSR subject field.
-
.Insecure.CR.Subject.CommonName: The Common Name (CN) from the CSR subject
-
.Insecure.CR.Subject.ExtraNames: The Extra Names from the CSR subject. This is an array of all subject distinguished names (DNs) in the CSR. Each contains an OID (
type
) and a value (value
). -
.Insecure.CR.RawSubject: The original subject asn.1 value from the CSR. Use this if your application requires that the certificate subject and CSR subject match exactly.
-
.Insecure.CR.PublicKey: The public key provided in the certificate request. You can check the request's key type with a conditional, like
{{- if typeIs "*rsa.PublicKey" .Insecure.CR.PublicKey }}
. You can check the request's key size with a conditional, like{{- if lt .Insecure.CR.PublicKey.Size 384 }}
. -
.Insecure.User: This property holds user-supplied data from the certificate request. Users can supply arbitrary values using
--set
or--set-file
flags instep ca certificate
.
You can also import values
from the "templateData"
object
in your provisioner's configuration block.
ASN.1 Values
The X.509 templating system includes a few functions for encoding ASN.1 values.
Use these functions to populate custom OID extensions
:
"extensions": [
{"id": "1.2.3.4", "critical": false, "value": {{ asn1Enc "int:3848281" | toJson }}}
]
When applied to template variables, these functions enable dynamic OID extensions:
{
"subject": {{ toJson .Subject }},
"sans": {{ toJson .SANs }},
{{- if typeIs "*rsa.PublicKey" .Insecure.CR.PublicKey }}
"keyUsage": ["keyEncipherment", "digitalSignature"],
{{- else }}
"keyUsage": ["digitalSignature"],
{{- end }}
"extKeyUsage": ["serverAuth", "clientAuth"],
"extensions": [
{"id": "1.2.3.4", "value": {{ asn1Marshal .AuthorizationCrt.NotAfter | toJson }}},
{"id": "1.2.3.5", "value": {{ asn1Set (asn1Marshal (first .Insecure.CR.DNSNames) "utf8") (asn1Enc "int:123456") | toJson }}},
{"id": "1.2.3.6", "value": {{ asn1Seq (asn1Enc "YubiKey") (asn1Enc "int:123456") | toJson }}}
]
}
Let's walk through one line of this template:
{"id": "1.2.3.6", "value": {{ asn1Seq (asn1Enc "YubiKey") (asn1Enc "int:123456") | toJson }}}
To render this line of the template:
asn1Enc "YubiKey"
encodes "YubiKey" as an ASN.1 Printable string, and returns the Base64asn1Enc "int:123456"
encodes 123 as an ASN.1 Integer, and returns the Base64asn1Seq
gets the resulting Base64 strings, decodes them, creates an ASN.1 SEQUENCE, and returns the Base64 of the SEQUENCE.- Finally,
toJson
returns a JSON string representation of the Base64
asn1Enc
Encodes a string into an ASN.1 value.
A data type can be supplied as a prefix, and ASN.1 Printable is the default data type:
asn1Enc "int:123"
asn1Enc "oid:1.2.3.4"
asn1Enc "foo"
asn1Enc .Subject.CommonName
This function supports the data types described here:
- printable: "printable:abcfoo" (or just "abcfoo")
- int: "int:12324"
- oid: "oid:1.2.3.4"
- utf8: "utf8:foobar"
- ia5: "ia5:abcfoo"
- numeric: "numeric:01 02 03345" (multiple space-separated ints)
- utc: "utc:2023-03-29T02:03:57Z" or "utc:2023-04-20 15:28:27.909088 -0700 PDT"
- generalized: "generalized:2023-03-29T02:03:57Z" or "generalized:2023-04-20 15:28:27.909088 -0700 PDT"
- bool: "bool:true" or "bool:false"
- raw: a special type that accepts a Base64 string
For more details on ASN.1 types and their meanings, see ASN.1 Types.
asn1Marshal
asn1Marshal
simplifies the encoding of Go variables into ASN.1.
For example, if you want to encode the Not After value in an X5C authorization certificate,
without converting it to a string first, asn1Marshal .AuthorizationCrt.NotAfter
will do the trick.
asn1Marshal
imitates Go's asn1.MarshalWithParams
function.
asn1Marshal .Token.iss
asn1Marshal .AuthorizationCrt.NotAfter "utc"
The second parameter is the ASN.1 data type. It will use Go's default transformation if no data type is supplied.
asn1Seq
Creates an ASN.1 SEQUENCE
of values.
asn1Seq (asn1Enc "foo") (asn1Enc "int:123") ...
asn1Set
Creates an ASN.1 SET
of values.
asn1Set (asn1Enc "foo") (asn1Enc "int:123") ...
SSH Templates
step-ca
also supports SSH certificate templates.
Here is step-ca
's default SSH certificate template:
{
"type": {{ toJson .Type }},
"keyId": {{ toJson .KeyID }},
"principals": {{ toJson .Principals }},
"extensions": {{ toJson .Extensions }},
"criticalOptions": {{ toJson .CriticalOptions }}
}
SSH Template Fields
The OpenSSH certificate protocol spec defines SSH certificate fields and the available Critical Options and Extensions.
Third parties can also add custom extensions.
See the complete list of fields supported by step-ca
SSH template.
SSH Template Variables
Here are the most relevant parameters available in SSH certificate template:
-
.Token: This property offers the best opportunity for injecting signed, secure data into the certificate. If a token was used to obtain the certificate,
this property contains token payload used in the certificate request. For example, when using an OAuth ID token,.Token.email
will contain the user's email address. To add custom data to.Token
, you could use custom OAuth claims, or sign your own JWTs. -
.Extensions: is a map containing extensions. The default value for
Extensions
is:{ "permit-X11-forwarding": "", "permit-agent-forwarding": "", "permit-port-forwarding": "", "permit-pty": "", "permit-user-rc": "" }
-
.CriticalOptions: is a map containing critical options. It is empty by default.
-
.Principals: For users, this value is derived from the OIDC token (when using the OIDC provisioner). For hosts, it is derived from the Instance Identity Document (when using the Cloud provisioners).
-
.KeyID: The key ID being requested.
-
.Type: The type of SSH certificate being requested. This will be
user
orhost
. -
.Insecure.CR*: SSH certificate requests to
step-ca
are not CSRs in the X.509 sense. So,step-ca
creates a virtual certificate request, and that's what this variable represents. -
.Insecure.CR.Principals: If you trust a host to register its own custom SANs (for example, when using the IID provisioner), then the host can pass several
--principal
values tostep ssh certificate
when registering the host, and use.Insecure.CR.Principals
to access those from a template. -
.Insecure.CR.Type: The type of SSH certificate being requested. This will be
user
orhost
. -
.Insecure.CR.KeyID: The key ID being requested.
-
.Insecure.User: This property holds user-supplied data from the certificate request. Users can supply arbitrary values using
--set
or--set-file
flags instep ssh certificate
.
You can also import parameter values from your provisioner's configuration block.
Time formatting
In addition to the time formatting functions offered by Sprig,
step-ca
has added some additional convenience functions for X.509 and SSH templates:
Function | Description |
---|---|
toTime 1719970524 | Returns a time.Time in UTC from a Unix epoch. |
formatTime (now) | Returns the UTC time as an RFC3339-formatted string. Example output: 2006-01-02T15:04:05Z07:00 |
parseTime | Returns the current time in UTC. |
parseTime "2024-07-03T01:30:39Z" | Parses a time string using RFC3339 format and returns a time.Time object. |
parseTime "time.UnixDate" "Tue Jul 2 18:31:04 PDT 2024" | Parses a time string using the time.UnixDate layout.Returns a time.Time object in UTC. |
parseTime "time.UnixDate" "Tue Jul 2 18:31:04 PDT 2024" "America/Los_Angeles" | Parses a time string using the time.UnixDate layout and a specified time zone identifier, returning a time.Time object. |
mustParseTime | Same as parseTime but returns an error if the time could not be parsed. |
toTimeLayout "RFC3339" | Returns the predefined layout string. Example output: 2006-01-02T15:04:05Z07:00 |
toTimeLayout "time.UnixDate" | Example output: Mon Jan _2 15:04:05 MST 2006 |
Configuring step-ca
to Use Templates
Within provisioner configuration, certificate templates can be set
under the property "options"
.
The following snippet shows a provisioner with custom X.509 and SSH templates:
{
"type": "JWK",
"name": "jane@doe.com",
"key": {
"use": "sig",
"kty": "EC",
"kid": "lq69QCCEwEhZys_wavar9RoqRLdJ58u_OGzJK0zswSU",
"crv": "P-256",
"alg": "ES256",
"x": "pt7T0n98qREZUkyUX6b4kXJ5FkJlIdiMfJaLFclZIng",
"y": "Pw1y1xqe4g4YARwyBSkEkcjNrtPYxdKlYDLI512t2_M"
},
"encryptedKey": "eyJhbGciOiJQQkVTMi1IUzI1NitBMTI4S1ciLCJjdHkiOiJqd2sranNvbiIsImVuYyI6IkEyNTZHQ00iLCJwMmMiOjEwMDAwMCwicDJzIjoiQ1dWZG5zWTR2bGZJbG9BQ1dOaUdNUSJ9.12dowlkvESpxJgrNJgP2ELDQz750HSh2w6Ux6BmatBE5-ybAJFFO7g.1cjU2-CTrV3gbUE7.m8a95nv4qLnN_K_PG7lzgzYXBGnw_aHCf-znJ34AZxzPy2QDGGEjN_V0jX3kvHH9AIg3cs8I8NRm__RDm2iezU5AhPoAHaqnPnZdKh0ReBZ4hNpYXUHlTPf4fRaCXXDQiKatxNzCMBpyqKpudf3xYUzZPRNMN78WM0ZeIzmv_jFzbryOpxD8bJ3Bnxa4e8Am_pPdAANHphodlKN2nDr4701OLKgitImm8RoA20sWdAI_LkTS_Abk_TqBo_3qOGdjmnRBtATFSu3BdQw5wZMjywfwCWKXUK_OUt-cjVIe11xUT43SoE8fR2GJJEKomAHP0vn0MUUMqY9P9icUejw.eEYI_H7WfrYDL4yhsnsJxg",
"claims": {
"enableSSHCA": true
}
"options": {
"x509": {
"templateFile": "templates/certs/x509/leaf.tpl",
"templateData": {
"OrganizationalUnit": "Engineering Team"
}
},
"ssh": {
"templateFile": "templates/certs/ssh/host.tpl"
}
}
}
-
options: object that allows configuration of provisioner options e.g. templates.
-
x509 or ssh: object for configuring X.509 or SSH certificate template options.
-
templateFile: path to a template stored on disk. You have a few options for how to define your path:
- absolute path: e.g.
/home/mariano/path/to/file.ptl
- relative to
$(step path)
: e.g.templates/certs/x509/leaf.tpl
the actual location of which would be$(step path)/templates/certs/x509/leaf.tpl
. - relative to the execution directory of
step-ca
: e.g../path/to/file.tpl
or../path/to/file.tpl
- absolute path: e.g.
-
templateData: defines variables that can be used in the template. In the example above, you will be able to use the defined organizational unit as the variable
{{ .OrganizationalUnit }}
, for example in a template like:{ "subject": { "organizationalUnit": {{ toJson .OrganizationalUnit }}, "commonName": {{ toJson .Subject.CommonName }} }, "sans": {{ toJson .SANs }}, "keyUsage": ["digitalSignature"], "extKeyUsage": ["serverAuth", "clientAuth"] }
-
template: set the full template in a string. The value can be the string representation of a JSON object, or you encoded in Base64. For example:
{ "x509": { "template": "{{ toJson .Insecure.CR }}", } }
Or using Base64:
{ "x509": { "template": "e3sgdG9Kc29uIC5JbnNlY3VyZS5DUiB9fQ==", } }
-
-
Basic X.509 Template Examples
Leaf Certificates
The most common type of certificate you'll want to create is a leaf (or end-entity) certificate; this is the type of certificate that your servers or clients will use.
The default leaf certificate template is:
{
"subject": {{ toJson .Subject }},
"sans": {{ toJson .SANs }},
{{- if typeIs "*rsa.PublicKey" .Insecure.CR.PublicKey }}
"keyUsage": ["keyEncipherment", "digitalSignature"],
{{- else }}
"keyUsage": ["digitalSignature"],
{{- end }}
"extKeyUsage": ["serverAuth", "clientAuth"]
}
We will explain what each block does, but first, let's look at what a rendered
template might look like. Imagine that you run the following command to create
a certificate that has jane@doe.com
as an email SAN.
step ca certificate jane@doe.com jane.crt
The rendered template (from which the X.509 certificate will be generated and signed) is:
{
"subject": {
"commonName": "jane@smallstep.com"
},
"sans": [{"type": "email", "value": "jane@smallstep.com"}],
"keyUsage": ["digitalSignature"],
"extKeyUsage": ["serverAuth", "clientAuth"]
}
Going back to the template above, we can see that the template has blocks
delimited by {{
and }}
. These are called actions - actions are
data evaluations or control structures. The ones in the default template are:
-
{{ toJson .Subject }}
: renders.Subject
as JSON.toJson
is a function in Sprig that encodes the passed item into JSON..Subject
is a variable available in all templates that contains the<subject>
parameter passed in the CLI, in this case,jane@smallstep.com
, and to be more precise, this value is available in.Subject.CommonName
. -
{{ toJson .SANs }}
: renders.SANs
(Subject Alternative Names) as JSON. The variable.SANs
is also available in all templates and contains the list ofSANs
passed from the CLI. If noSANs
are specified, the.Subject.CommonName
will be used as a defaultSAN
(e.g.jane@smallstep.com
in our example). If you add moreSANs
using the flag--san
,step-ca
will auto-assign the correct SANtype
(dns
,ip
,uri
, oremail
). -
{{- if typeIs "*rsa.PublicKey" .Insecure.CR.PublicKey }}
: defines a condition based on the key type in the certificate request.typeIs
is a Sprig function that returns true if the type of the data in.Insecure.CR.PublicKey
matches*rsa.PublicKey
, this is the Go specific type for RSA public keys. If the condition is true, thekeyUsage
is set tokeyEncipherment
anddigitalSignature
, otherwise justdigitalSignature
.keyEncipherment
is used in the RSA key exchange protocol, and it must only be set if an RSA key is used..Insecure.CR
is a structure that represents the certificate (signing) request or CSR, from it we can extract SANs, the subject, extensions, etc.
Finally we also set the extended key usage serverAuth
and clientAuth
so the
certificate can be used in a server application or in a client for mTLS.
Intermediate Certificates
An intermediate certificate is a certificate that can be used to sign another certificate, and it has itself been signed by another intermediate certificate or by a root certificate.
The default template for an intermediate certificate is:
{
"subject": {{ toJson .Subject }},
"keyUsage": ["certSign", "crlSign"],
"basicConstraints": {
"isCA": true,
"maxPathLen": 0
}
}
This template is most commonly used when initializing a CA.
The keyUsage
certSign allows a certificate to sign other certificates.
crlSign allows a certificate to sign Certificate Revocation Lists or CRLs.
In basicConstraints
we say that this certificate is a CA, and the path length
constraint, maxPathLen
, is maximum number of non-self-issued intermediate
certificates that may follow this certificate in a valid certification path. An
intermediate with a maxPathLen
set to 0
won't be able to sign another
intermediate certificate. To be precise, it can sign it, but the new
intermediate won't be trusted by clients, so it won't be useful.
Knowing this we can also guess that the intermediate certificate's issuer had at
least a maxPathLen
of 1
. So if you want to use an intermediate certificate
that can sign other intermediates, make sure to set more permissive maxPathLen
in the root and intermediate certificate used.
The command step certificate create
with the --profile intermediate-ca
flag
uses the template above, but if you want to create an intermediate with a custom
template you can run:
step certificate create --template intermediate.tpl \
--ca ~/.step/certs/root_ca.crt --ca-key ~/.step/secrets/root_ca_key \
"ACME Intermediate CA" intermediate_ca.crt intermediate_ca_key
Root Certificates
A root certificate is a self-signed certificate used to sign other certificates. The default root certificate template is:
{
"subject": {{ toJson .Subject }},
"issuer": {{ toJson .Subject }},
"keyUsage": ["certSign", "crlSign"],
"basicConstraints": {
"isCA": true,
"maxPathLen": 1
}
}
Here we are specifying that the issuer is equivalent to the subject, and the maxPathLen
is set to
1
, so it can sign intermediate certificates with maxPathLen
set to 0
. If
you want to allow intermediates to sign other intermediates, you need to set the
maxPathLen
to at least 2
. It's also possible to not limit the number of
intermediates that can follow this certificate if we set maxPathLen
to -1
,
and then create intermediates that set a specific limit, i.e. 0
for those that
can sign only leaf certificates and 1
for those that can sign at most one downstream
intermediate certificate.
The command step certificate create
with the --profile root-ca
flag uses the
above template. To create a custom root certificate, you can run:
step certificate create --template root.tpl "ACME Root CA" root_ca.crt root_ca_key
Advanced X.509 Template Examples
In the previous section, we learned how to create and use the basic templates, but we've just scratched the surface of what's possible. With templates, you can customize and parameterize every aspect of an X.509 or SSH certificate.
Below, we'll walk through a few advanced templating examples.
Certificate Request
Let's start with one of the shortest templates:
{{ toJson .Insecure.CR }}
This template produces a signed certificate that contains all the information present in the certificate request (with a few caveats). All SANs and extensions present in the certificate request will be in the final certificate.
The only modifications that a signed certificate might have (over a CSR), would be in the
certificate subject on step-ca
if a template is provided in the ca.json
, and
it will set the Subject Alternative Name extension to critical if the subject is
empty.
User-Provided Variables in Signing Requests
In step-ca
, X.509 templates can also be parameterized with variables that will be
provided by step
at certificate creation time. A common use case for
variables is when you receive a CSR from another team, or a CSR embedded in
hardware, and you need to define a SAN for it.
For example, below is an X.509 template that accepts the user variable dnsName
but falls back to the default leaf template if it's not present:
{
"subject": {{ toJson .Subject }},
{{- if .Insecure.User.dnsName }}
"dnsNames": {{ toJson .Insecure.User.dnsName}},
{{- else }}
"sans": {{ toJson .SANs }},
{{- end }}
{{- if typeIs "*rsa.PublicKey" .Insecure.CR.PublicKey }}
"keyUsage": ["keyEncipherment", "digitalSignature"],
{{- else }}
"keyUsage": ["digitalSignature"],
{{- end }}
"extKeyUsage": ["serverAuth", "clientAuth"]
}
You can then use step
to sign a CSR like so:
$ step ca sign --set "dnsName=backend.example.com" input.csr output.crt
# Or
$ echo '{"dnsName": "backend.example.com"}' > vars.json
$ step ca sign --set-file vars.json input.csr output.crt
Both flags, --set <name=value>
and --set-file <path>
are available in
step ca certificate
and step ca sign
. If you need to pass more than one
variable, you can use --set
multiple times or use a JSON file with multiple
properties.
It's worth mentioning the while we used "dnsNames"
instead of "sans"
in the example above, both can
be used. "dnsNames"
is an array of strings (or just one string if only one is
required), while "sans"
is an array of objects like:
[
{"type": "dns", "value": "backend.example.com"},
{"type": "email", "value": "jane@example.com"},
{"type": "ip", "value": "10.0.1.10"},
{"type": "uri", "value": "https://backend.example.com/root.crt"},
]
The variable .SANs
is generated by the provisioners with the values of the
trusted names.
Besides "dnsNames"
, you can also use "emailAddresses"
, "ipAddresses"
, and
"uris"
.
Adding Name Constraints
Name constraints are useful in intermediate certificates when you want to limit the validity of certificates that an intermediate can sign. If we want to only allow DNS name like *.example.com we can generate an intermediate with the template:
{
"subject": {{ toJson .Subject }},
"keyUsage": ["certSign", "crlSign"],
"basicConstraints": {
"isCA": true,
"maxPathLen": 0
},
"nameConstraints": {
"critical": true,
"permittedDNSDomains": ["example.com"]
}
}
Given a root certificate and a key, you can generate the intermediate with:
step certificate create —template intermediate.tpl --ca root_ca.crt --ca-key root_ca_key \
“Intermediate CA” intermediate.crt intermediate.key
Besides "permittedDNSDomains"
, the "nameConstraints"
property accepts all
the following properties:
{
"nameConstraints": {
"critical": false,
"permittedDNSDomains": ["doe.com"],
"excludedDNSDomains": ["doe.org"],
"permittedIPRanges": ["1.2.3.0/24"],
"excludedIPRanges": ["3.2.1.0/24"],
"permittedEmailAddresses": ["jane@doe.com"],
"excludedEmailAddresses": ["jane@doe.org"],
"permittedURIDomains": ["https://doe.com"],
"excludedURIDomains": ["https://doe.org"],
}
}
Remember that in certificate templates, if an array only has one member, you can write it as a string:
{
"nameConstraints": {
"critical": true,
"permittedDNSDomains": "example.com"
}
}
Arbitrary X.509 Extensions
SANs, key usages, extended key usages, basic constraints, and name constraints, are all extensions in an X.509 certificate. Templates provide an easy way for assigning extensions without the need of using the actual extension bytes.
The same thing is also possible with top-level certificate template properties like:
-
unknownExtKeyUsage: defines custom extended key usages using ASN.1 object identifiers; this is useful if the templates do not support a string version of the extended key usage (RFC 5280, 4.2.1.12).
-
ocspServer: sets a list of OCSP servers in the Authority Information Access extension (RFC 5280, 4.2.2.1).
-
issuingCertificateURL: defines the issuing certificate URL in the Authority Information Access extension (RFC 5280, 4.2.2.1).
-
crlDistributionPoints: defines URLs with the certificate revocation list (RFC 5280, 4.2.1.13)).
-
policyIdentifiers: defines a list of ASN.1 object identifiers with certificate policies (RFC 5280, 4.2.1.4).
But if you need to create your own extension, or use an unsupported one, you can also write a custom extension like:
{
"extensions": [
{"id": "1.2.3.4", "critical": false, "value": "Y3VzdG9tIGV4dGVuc2lvbiB2YWx1ZQ=="}
]
}
The crux here is that the value of the extension is the Base64 encoding of the actual bytes that go into that extension, so if you are encoding a structure in your extension using the ASN.1 encoding, you will have to put the Base64 version of the encoded bytes.
X.509 OpenVPN certificates
One interesting use case for certificate templates is the signing of OpenVPN certificates. These certificates require specific key usages not available in the default templates.
This is a template you can use in a provisioner signing OpenVPN client certificates:
{
"subject": {"commonName": {{ toJson .Insecure.CR.Subject.CommonName }}},
"sans": {{ toJson .SANs }},
"keyUsage": ["digitalSignature", "keyAgreement"],
"extKeyUsage": ["clientAuth"]
}
And the following template can be used for a provisioner signing OpenVPN server certificates:
{
"subject": {{ toJson .Subject }},
"sans": {{ toJson .SANs }},
"keyUsage": ["digitalSignature", "keyEncipherment", "keyAgreement"],
"extKeyUsage": ["serverAuth"]
}
A full configuration of step-ca
and OpenVPN is available in our blog post
"Announcing X.509 Certificate
Flexibility"
SSH Template Examples
GitHub SSH Certificates
With GitHub Enterprise Cloud or GitHub Enterprise Server, you can configure GitHub to use SSH certificates.
Specifically, GitHub will trust an SSH Certificate Authority for your team.
But, to get it to work, you have to generate custom SSH certificates for your users
using a login@github.com
extension,
where the value of the extension is their GitHub Username.
This custom extension value authenticates your users to GitHub Enterprise.
Which is great: The same certificates that let you SSH into your servers now also let you push code.
Here's an SSH template that supports the GitHub custom SSH certificate extension:
{
"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 this template, you'd need to add a ghu
("GitHub Username") custom claim to the token that you send the CA to authenticate your certificate request.
If you use an OIDC provisioner with this template,
you can configure your identity provider to serve a custom claim labeled ghu
in your ID token.
Our blog post "Clever Uses of SSH Certificate Templates" has more details on this option.
The JWK and Cloud provisioners also use tokens.
Here's an example of a Bash test script that
decrypts your JWK provisioner key,
and uses it to sign a token that step-ca
will accept for an SSH certificate:
#!/bin/bash
#
# This script is for testing purposes only.
#
# Your CA hostname and port
CA=ca.local:443
# Key ID: This is the "kid" value from your JWK provisioner configuration
KID=R0a0VGbbOe3j6ol5jOdV3qfiigjcezk0LG9K9Cp7mrg
# Issuer: The "name" of your JWK provisioner
ISS="carl@smallstep.com"
if [ "$#" -ne 2 ]; then
name=$(basename $0)
echo "Usage: $name <princiap> <github-username>"
exit 1
fi
# Grab the key to encrypt
KEY=`curl --cacert ~/.step-ssh-templates/certs/root_ca.crt -s https://${CA}/1.0/provisioners/${KID}/encrypted-key | jq -r .key | step crypto jwe decrypt`
echo '{"step": {"ssh":{"certType": "user", "keyID": "'$1'"}}, "ghu":"'$2'"}' | step crypto jwt sign --key <(echo $KEY) --iss "$ISS" --aud "https://${CA}/1.0/sign" -sub $1 -exp $(date -v+5M +"%s")
Here's the script used in concert with the GitHub SSH template above:
$ TOKEN=$(./gh_token.sh carl tashian)
Please enter the password to decrypt the content encryption key: ...
$ echo $TOKEN | step crypto jwt inspect --insecure
{
"header": {
"alg": "ES256",
"kid": "R0a0VGbbOe3j6ol5jOdV3qfiigjcezk0LG9K9Cp7mrg",
"typ": "JWT"
},
"payload": {
"aud": "https://localhost:4430/1.0/sign",
"exp": 1606870196,
"ghu": "tashian",
"iat": 1606869896,
"iss": "carl@smallstep.com",
"jti": "6d62922871db260e54d97fe1f2561440177ae579e31baa954128e0fba155b4db",
"nbf": 1606869896,
"step": {
"ssh": {
"certType": "user",
"keyID": "carl"
}
},
"sub": "carl"
},
"signature": "RdYo70cCR7tMZBl8VLbxVpoCDlNED2wpRw8uPV1rNkDENBbXqml1h-TnWG23dJ4zKzPhAuO2Vk7HdOUC2q0mNg"
}
$ step ssh certificate carl github --token $TOKEN
✔ CA: https://localhost:4430
Please enter the password to encrypt the private key:
✔ Private Key: github
✔ Public Key: github.pub
✔ Certificate: github-cert.pub
✔ SSH Agent: yes
$ step ssh inspect github-cert.pub
github-cert.pub:
Type: ecdsa-sha2-nistp256-cert-v01@openssh.com user certificate
Public key: ECDSA-CERT SHA256:SdcgsNKaQbhYveanJYjmJk4eLN3+CBKUKkRGv4xYn5U
Signing CA: ECDSA SHA256:MKwRQ/SDKk/pCJbbCk5bfhZACjSjv7uZXLyc5n4Wx6k
Key ID: "carl"
Serial: 886026644913671581
Valid: from 2020-12-01T16:40:32 to 2020-12-02T08:41:32
Principals:
carl
Critical Options: (none)
Extensions:
login@github.com tashian
In production, you would not want to hand out your JWK provisioner password.
But you could create a token server that interoperates with step-ca
and injects secure values into the token.
SSH Group Accounts
Let's say your organization has shared group accounts on your hosts (eg. "dba" or "devops"). You'd want to be able to issue people in those groups a certificate that contains their user principals ("carl", "carl@smallstep.com"), plus principals for any group accounts they need access to.
Let's use the OIDC ID token to inject additional principals into an SSH user certificate.
First, configure a custom groups
claim with your identity provider, and add the claim to the payload of the ID token.
In this example, we're assuming the groups
claim contains a space-separated list of possible group accounts.
Then, use the following template to merge the group accounts with the user's own principals (derived from the email
claim):
{
"type": {{ toJson .Type }},
"keyId": {{ toJson .KeyID }},
"principals": {{ toJson ((concat .Principals (splitList " " .Token.groups)) | uniq) }},
"criticalOptions": {{ toJson .CriticalOptions }},
"extensions": {{ toJson .Extensions }}
}
This is a good example of the use of Sprig functions in templates.
Finally, test out your flow with step ssh login
, and use step ssh inspect
to check the principals of the certificate you're issued by the CA.
Even More Templates
To delve deeper into certificate templates and all the options, the best source of information is the code docs for the go.step.sm/crypto/x509util package:
-
The Certificate object represents a certificate template with all its properties.
-
The CertificateRequest object represents all properties for a CSR template that can be used in
step certificate create --csr
. -
All key usages and supported extended key usages without using
"unknownExtKeyUsage"
are available here.
More information and more examples are available in the blog posts "Announcing X.509 Certificate Flexibility" and "Clever Uses of SSH Certificate Templates".