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

View file

@ -21,14 +21,23 @@ func (fn certificateOptionsFunc) Options(so Options) []x509util.Option {
type ProvisionerOptions struct {
Template string `json:"template"`
TemplateFile string `json:"templateFile`
TemplateFile string `json:"templateFile"`
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
// 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) {
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 data == nil {
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 certificateOptionsFunc(func(so Options) []x509util.Option {
// We're not provided user data without custom templates.
if o == nil || (o.Template == "" && o.TemplateFile == "") {
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.
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.
func (m *MultiURL) UnmarshalJSON(data []byte) error {
ms, err := unmarshalMultiString(data)

View file

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

View file

@ -41,6 +41,12 @@ func (t TemplateData) SetSubject(v Subject) {
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) {
t[SANsKey] = CreateSANs(sans)
}
@ -53,6 +59,9 @@ func (t TemplateData) SetCertificateRequest(cr *x509.CertificateRequest) {
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 = `{
"subject": {{ toJson .Subject }},
"sans": {{ toJson .SANs }},
@ -60,6 +69,30 @@ const DefaultLeafTemplate = `{
"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 = `{
"subject": {{ toJson .Subject }},
"keyUsage": ["certSign", "crlSign"],
@ -69,6 +102,8 @@ const DefaultIntermediateTemplate = `{
}
}`
// DefaultRootTemplate is a template that can be used to generate a root
// certificate.
const DefaultRootTemplate = `{
"subject": {{ toJson .Subject }},
"issuer": {{ toJson .Subject }},