Add template support for AWS provisioner.

This commit is contained in:
Mariano Cano 2020-07-13 12:47:26 -07:00
parent 49b9aa6e3f
commit 13b704aeed
5 changed files with 83 additions and 15 deletions

View file

@ -17,6 +17,7 @@ import (
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/smallstep/certificates/errs" "github.com/smallstep/certificates/errs"
"github.com/smallstep/certificates/x509util"
"github.com/smallstep/cli/jose" "github.com/smallstep/cli/jose"
) )
@ -125,13 +126,14 @@ type awsInstanceIdentityDocument struct {
// https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/instance-identity-documents.html // https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/instance-identity-documents.html
type AWS struct { type AWS struct {
*base *base
Type string `json:"type"` Type string `json:"type"`
Name string `json:"name"` Name string `json:"name"`
Accounts []string `json:"accounts"` Accounts []string `json:"accounts"`
DisableCustomSANs bool `json:"disableCustomSANs"` DisableCustomSANs bool `json:"disableCustomSANs"`
DisableTrustOnFirstUse bool `json:"disableTrustOnFirstUse"` DisableTrustOnFirstUse bool `json:"disableTrustOnFirstUse"`
InstanceAge Duration `json:"instanceAge,omitempty"` InstanceAge Duration `json:"instanceAge,omitempty"`
Claims *Claims `json:"claims,omitempty"` Claims *Claims `json:"claims,omitempty"`
Options *ProvisionerOptions `json:"options,omitempty"`
claimer *Claimer claimer *Claimer
config *awsConfig config *awsConfig
audiences Audiences audiences Audiences
@ -276,14 +278,20 @@ func (p *AWS) AuthorizeSign(ctx context.Context, token string) ([]SignOption, er
} }
doc := payload.document doc := payload.document
// Template options
data := x509util.NewTemplateData()
data.SetCommonName(payload.Claims.Subject)
// Enforce known CN and default DNS and IP if configured. // Enforce known CN and default DNS and IP if configured.
// By default we'll accept the CN and SANs in the CSR. // By default we'll accept the CN and SANs in the CSR.
// There's no way to trust them other than TOFU. // There's no way to trust them other than TOFU.
var so []SignOption var so []SignOption
if p.DisableCustomSANs { if p.DisableCustomSANs {
so = append(so, dnsNamesValidator([]string{ dnsName := fmt.Sprintf("ip-%s.%s.compute.internal", strings.Replace(doc.PrivateIP, ".", "-", -1), doc.Region)
fmt.Sprintf("ip-%s.%s.compute.internal", strings.Replace(doc.PrivateIP, ".", "-", -1), doc.Region), data.SetSANs([]string{dnsName, doc.PrivateIP})
}))
so = append(so, dnsNamesValidator([]string{dnsName}))
so = append(so, ipAddressesValidator([]net.IP{ so = append(so, ipAddressesValidator([]net.IP{
net.ParseIP(doc.PrivateIP), net.ParseIP(doc.PrivateIP),
})) }))
@ -291,7 +299,13 @@ func (p *AWS) AuthorizeSign(ctx context.Context, token string) ([]SignOption, er
so = append(so, urisValidator(nil)) so = append(so, urisValidator(nil))
} }
templateOptions, err := CustomTemplateOptions(p.Options, data, x509util.DefaultIIDLeafTemplate)
if err != nil {
return nil, errs.Wrap(http.StatusInternalServerError, err, "aws.AuthorizeSign")
}
return append(so, return append(so,
templateOptions,
// modifiers / withOptions // modifiers / withOptions
newProvisionerExtensionOption(TypeAWS, p.Name, doc.AccountID, "InstanceID", doc.InstanceID), newProvisionerExtensionOption(TypeAWS, p.Name, doc.AccountID, "InstanceID", doc.InstanceID),
profileDefaultDuration(p.claimer.DefaultTLSCertDuration()), profileDefaultDuration(p.claimer.DefaultTLSCertDuration()),

View file

@ -21,14 +21,23 @@ func (fn certificateOptionsFunc) Options(so Options) []x509util.Option {
type ProvisionerOptions struct { type ProvisionerOptions struct {
Template string `json:"template"` Template string `json:"template"`
TemplateFile string `json:"templateFile` TemplateFile string `json:"templateFile"`
TemplateData json.RawMessage `json:"templateData"` TemplateData json.RawMessage `json:"templateData"`
} }
// TemplateOptions generate a CertificateOptions with the template and data // TemplateOptions generates a CertificateOptions with the template and data
// defined in the ProvisionerOptions, the provisioner generated data, and the // defined in the ProvisionerOptions, the provisioner generated data, and the
// user data provided in the request. // user data provided in the request. If no template has been provided,
// x509util.DefaultLeafTemplate will be used.
func TemplateOptions(o *ProvisionerOptions, data x509util.TemplateData) (CertificateOptions, error) { func TemplateOptions(o *ProvisionerOptions, data x509util.TemplateData) (CertificateOptions, error) {
return CustomTemplateOptions(o, data, x509util.DefaultLeafTemplate)
}
// CustomTemplateOptions generates a CertificateOptions with the template, data
// defined in the ProvisionerOptions, the provisioner generated data and the
// user data provided in the request. If no template has been provided in the
// ProvisionerOptions, the given template will be used.
func CustomTemplateOptions(o *ProvisionerOptions, data x509util.TemplateData, defaultTemplate string) (CertificateOptions, error) {
if o != nil { if o != nil {
if data == nil { if data == nil {
data = x509util.NewTemplateData() data = x509util.NewTemplateData()
@ -40,14 +49,13 @@ func TemplateOptions(o *ProvisionerOptions, data x509util.TemplateData) (Certifi
return nil, errors.Wrap(err, "error unmarshaling template data") return nil, errors.Wrap(err, "error unmarshaling template data")
} }
} }
} }
return certificateOptionsFunc(func(so Options) []x509util.Option { return certificateOptionsFunc(func(so Options) []x509util.Option {
// We're not provided user data without custom templates. // We're not provided user data without custom templates.
if o == nil || (o.Template == "" && o.TemplateFile == "") { if o == nil || (o.Template == "" && o.TemplateFile == "") {
return []x509util.Option{ return []x509util.Option{
x509util.WithTemplate(x509util.DefaultLeafTemplate, data), x509util.WithTemplate(defaultTemplate, data),
} }
} }

View file

@ -82,6 +82,15 @@ func (m *MultiIPNet) UnmarshalJSON(data []byte) error {
// into a []*url.URL. // into a []*url.URL.
type MultiURL []*url.URL type MultiURL []*url.URL
// MarshalJSON implements the json.Marshaler interface for MultiURL.
func (m MultiURL) MarshalJSON() ([]byte, error) {
urls := make([]string, len(m))
for i, u := range m {
urls[i] = u.String()
}
return json.Marshal(urls)
}
// UnmarshalJSON implements the json.Unmarshaler interface for MultiURL. // UnmarshalJSON implements the json.Unmarshaler interface for MultiURL.
func (m *MultiURL) UnmarshalJSON(data []byte) error { func (m *MultiURL) UnmarshalJSON(data []byte) error {
ms, err := unmarshalMultiString(data) ms, err := unmarshalMultiString(data)

View file

@ -3,6 +3,7 @@ package x509util
import ( import (
"bytes" "bytes"
"crypto/x509" "crypto/x509"
"fmt"
"io/ioutil" "io/ioutil"
"text/template" "text/template"
@ -42,6 +43,7 @@ func WithTemplate(text string, data TemplateData) Option {
if err := tmpl.Execute(buf, data); err != nil { if err := tmpl.Execute(buf, data); err != nil {
return errors.Wrapf(err, "error executing template") return errors.Wrapf(err, "error executing template")
} }
fmt.Println(buf.String())
o.CertBuffer = buf o.CertBuffer = buf
return nil return nil
} }

View file

@ -41,6 +41,12 @@ func (t TemplateData) SetSubject(v Subject) {
t[SubjectKey] = v t[SubjectKey] = v
} }
func (t TemplateData) SetCommonName(cn string) {
s, _ := t[SubjectKey].(Subject)
s.CommonName = cn
t[SubjectKey] = s
}
func (t TemplateData) SetSANs(sans []string) { func (t TemplateData) SetSANs(sans []string) {
t[SANsKey] = CreateSANs(sans) t[SANsKey] = CreateSANs(sans)
} }
@ -53,6 +59,9 @@ func (t TemplateData) SetCertificateRequest(cr *x509.CertificateRequest) {
t[CertificateRequestKey] = newCertificateRequest(cr) t[CertificateRequestKey] = newCertificateRequest(cr)
} }
// DefaultLeafTemplate is the default templated used to generate a leaf
// certificate. The keyUsage "keyEncipherment" is special and it will be only
// used for RSA keys.
const DefaultLeafTemplate = `{ const DefaultLeafTemplate = `{
"subject": {{ toJson .Subject }}, "subject": {{ toJson .Subject }},
"sans": {{ toJson .SANs }}, "sans": {{ toJson .SANs }},
@ -60,6 +69,30 @@ const DefaultLeafTemplate = `{
"extKeyUsage": ["serverAuth", "clientAuth"] "extKeyUsage": ["serverAuth", "clientAuth"]
}` }`
// DefaultIIDLeafTemplate is the template used by default on instance identity
// provisioners like AWS, GCP or Azure. By default, those provisioners allow the
// SANs provided in the certificate request, but the option `DisableCustomSANs`
// can be provided to force only the verified domains, if the option is true
// `.SANs` will be set with the verified domains.
//
// The keyUsage "keyEncipherment" is special and it will be only used for RSA
// keys.
const DefaultIIDLeafTemplate = `{
"subject": {{ toJson .Subject }},
{{- if .SANs }}
"sans": {{ toJson .SANs }},
{{- else }}
"dnsNames": {{ toJson .CR.DNSNames }},
"emailAddresses": {{ toJson .CR.EmailAddresses }},
"ipAddresses": {{ toJson .CR.IPAddresses }},
"uris": {{ toJson .CR.URIs }},
{{- end }}
"keyUsage": ["keyEncipherment", "digitalSignature"],
"extKeyUsage": ["serverAuth", "clientAuth"]
}`
// DefaultIntermediateTemplate is a template that can be used to generate an
// intermediate certificate.
const DefaultIntermediateTemplate = `{ const DefaultIntermediateTemplate = `{
"subject": {{ toJson .Subject }}, "subject": {{ toJson .Subject }},
"keyUsage": ["certSign", "crlSign"], "keyUsage": ["certSign", "crlSign"],
@ -69,6 +102,8 @@ const DefaultIntermediateTemplate = `{
} }
}` }`
// DefaultRootTemplate is a template that can be used to generate a root
// certificate.
const DefaultRootTemplate = `{ const DefaultRootTemplate = `{
"subject": {{ toJson .Subject }}, "subject": {{ toJson .Subject }},
"issuer": {{ toJson .Subject }}, "issuer": {{ toJson .Subject }},