diff --git a/authority/authority.go b/authority/authority.go index f396c588..4eacfad7 100644 --- a/authority/authority.go +++ b/authority/authority.go @@ -16,6 +16,7 @@ import ( adminDBNosql "github.com/smallstep/certificates/authority/admin/db/nosql" "github.com/smallstep/certificates/authority/administrator" "github.com/smallstep/certificates/authority/config" + "github.com/smallstep/certificates/authority/policy" "github.com/smallstep/certificates/authority/provisioner" "github.com/smallstep/certificates/cas" casapi "github.com/smallstep/certificates/cas/apiv1" @@ -75,6 +76,11 @@ type Authority struct { sshGetHostsFunc func(ctx context.Context, cert *x509.Certificate) ([]config.Host, error) getIdentityFunc provisioner.GetIdentityFunc + // Policy engines + x509Policy policy.X509Policy + sshUserPolicy policy.UserPolicy + sshHostPolicy policy.HostPolicy + adminMutex sync.RWMutex } @@ -539,6 +545,21 @@ func (a *Authority) init() error { a.templates.Data["Step"] = tmplVars } + // Initialize the x509 allow/deny policy engine + if a.x509Policy, err = policy.NewX509PolicyEngine(a.config.AuthorityConfig.Policy.GetX509Options()); err != nil { + return err + } + + // // Initialize the SSH allow/deny policy engine for host certificates + if a.sshHostPolicy, err = policy.NewSSHHostPolicyEngine(a.config.AuthorityConfig.Policy.GetSSHOptions()); err != nil { + return err + } + + // // Initialize the SSH allow/deny policy engine for user certificates + if a.sshUserPolicy, err = policy.NewSSHUserPolicyEngine(a.config.AuthorityConfig.Policy.GetSSHOptions()); err != nil { + return err + } + // JWT numeric dates are seconds. a.startTime = time.Now().Truncate(time.Second) // Set flag indicating that initialization has been completed, and should diff --git a/authority/config/config.go b/authority/config/config.go index 589b5bbf..0f6120f9 100644 --- a/authority/config/config.go +++ b/authority/config/config.go @@ -8,6 +8,7 @@ import ( "time" "github.com/pkg/errors" + "github.com/smallstep/certificates/authority/policy" "github.com/smallstep/certificates/authority/provisioner" cas "github.com/smallstep/certificates/cas/apiv1" "github.com/smallstep/certificates/db" @@ -90,6 +91,7 @@ type AuthConfig struct { Admins []*linkedca.Admin `json:"-"` Template *ASN1DN `json:"template,omitempty"` Claims *provisioner.Claims `json:"claims,omitempty"` + Policy *policy.Options `json:"policy,omitempty"` DisableIssuedAtCheck bool `json:"disableIssuedAtCheck,omitempty"` Backdate *provisioner.Duration `json:"backdate,omitempty"` EnableAdmin bool `json:"enableAdmin,omitempty"` diff --git a/authority/policy/options.go b/authority/policy/options.go new file mode 100644 index 00000000..f57f3bcf --- /dev/null +++ b/authority/policy/options.go @@ -0,0 +1,170 @@ +package policy + +type Options struct { + X509 *X509PolicyOptions `json:"x509,omitempty"` + SSH *SSHPolicyOptions `json:"ssh,omitempty"` +} + +func (o *Options) GetX509Options() *X509PolicyOptions { + if o == nil { + return nil + } + return o.X509 +} + +func (o *Options) GetSSHOptions() *SSHPolicyOptions { + if o == nil { + return nil + } + return o.SSH +} + +type X509PolicyOptionsInterface interface { + GetAllowedNameOptions() *X509NameOptions + GetDeniedNameOptions() *X509NameOptions +} + +type X509PolicyOptions struct { + // AllowedNames ... + AllowedNames *X509NameOptions `json:"allow,omitempty"` + + // DeniedNames ... + DeniedNames *X509NameOptions `json:"deny,omitempty"` +} + +// X509NameOptions models the X509 name policy configuration. +type X509NameOptions struct { + DNSDomains []string `json:"dns,omitempty"` + IPRanges []string `json:"ip,omitempty"` + EmailAddresses []string `json:"email,omitempty"` + URIDomains []string `json:"uri,omitempty"` +} + +// HasNames checks if the AllowedNameOptions has one or more +// names configured. +func (o *X509NameOptions) HasNames() bool { + return len(o.DNSDomains) > 0 || + len(o.IPRanges) > 0 || + len(o.EmailAddresses) > 0 || + len(o.URIDomains) > 0 +} + +type SSHPolicyOptionsInterface interface { + GetAllowedUserNameOptions() *SSHNameOptions + GetDeniedUserNameOptions() *SSHNameOptions + GetAllowedHostNameOptions() *SSHNameOptions + GetDeniedHostNameOptions() *SSHNameOptions +} + +type SSHPolicyOptions struct { + // User contains SSH user certificate options. + User *SSHUserCertificateOptions `json:"user,omitempty"` + + // Host contains SSH host certificate options. + Host *SSHHostCertificateOptions `json:"host,omitempty"` +} + +// GetAllowedNameOptions returns AllowedNames, which models the +// SANs that ... +func (o *X509PolicyOptions) GetAllowedNameOptions() *X509NameOptions { + if o == nil { + return nil + } + return o.AllowedNames +} + +// GetDeniedNameOptions returns the DeniedNames, which models the +// SANs that ... +func (o *X509PolicyOptions) GetDeniedNameOptions() *X509NameOptions { + if o == nil { + return nil + } + return o.DeniedNames +} + +func (o *SSHPolicyOptions) GetAllowedUserNameOptions() *SSHNameOptions { + if o == nil { + return nil + } + if o.User == nil { + return nil + } + return o.User.AllowedNames +} + +func (o *SSHPolicyOptions) GetDeniedUserNameOptions() *SSHNameOptions { + if o == nil { + return nil + } + if o.User == nil { + return nil + } + return o.User.DeniedNames +} + +func (o *SSHPolicyOptions) GetAllowedHostNameOptions() *SSHNameOptions { + if o == nil { + return nil + } + if o.Host == nil { + return nil + } + return o.Host.AllowedNames +} + +func (o *SSHPolicyOptions) GetDeniedHostNameOptions() *SSHNameOptions { + if o == nil { + return nil + } + if o.Host == nil { + return nil + } + return o.Host.DeniedNames +} + +// SSHUserCertificateOptions is a collection of SSH user certificate options. +type SSHUserCertificateOptions struct { + // AllowedNames contains the names the provisioner is authorized to sign + AllowedNames *SSHNameOptions `json:"allow,omitempty"` + // DeniedNames contains the names the provisioner is not authorized to sign + DeniedNames *SSHNameOptions `json:"deny,omitempty"` +} + +// SSHHostCertificateOptions is a collection of SSH host certificate options. +// It's an alias of SSHUserCertificateOptions, as the options are the same +// for both types of certificates. +type SSHHostCertificateOptions SSHUserCertificateOptions + +// SSHNameOptions models the SSH name policy configuration. +type SSHNameOptions struct { + DNSDomains []string `json:"dns,omitempty"` + IPRanges []string `json:"ip,omitempty"` + EmailAddresses []string `json:"email,omitempty"` + Principals []string `json:"principal,omitempty"` +} + +// GetAllowedNameOptions returns the AllowedSSHNameOptions, which models the +// names that a provisioner is authorized to sign SSH certificates for. +func (o *SSHUserCertificateOptions) GetAllowedNameOptions() *SSHNameOptions { + if o == nil { + return nil + } + return o.AllowedNames +} + +// GetDeniedNameOptions returns the DeniedSSHNameOptions, which models the +// names that a provisioner is NOT authorized to sign SSH certificates for. +func (o *SSHUserCertificateOptions) GetDeniedNameOptions() *SSHNameOptions { + if o == nil { + return nil + } + return o.DeniedNames +} + +// HasNames checks if the SSHNameOptions has one or more +// names configured. +func (o *SSHNameOptions) HasNames() bool { + return len(o.DNSDomains) > 0 || + len(o.EmailAddresses) > 0 || + len(o.Principals) > 0 +} diff --git a/authority/policy/policy.go b/authority/policy/policy.go new file mode 100644 index 00000000..403ac0b7 --- /dev/null +++ b/authority/policy/policy.go @@ -0,0 +1,134 @@ +package policy + +import ( + "fmt" + + "github.com/smallstep/certificates/policy" +) + +// X509Policy is an alias for policy.X509NamePolicyEngine +type X509Policy policy.X509NamePolicyEngine + +// UserPolicy is an alias for policy.SSHNamePolicyEngine +type UserPolicy policy.SSHNamePolicyEngine + +// HostPolicy is an alias for policy.SSHNamePolicyEngine +type HostPolicy policy.SSHNamePolicyEngine + +// NewX509PolicyEngine creates a new x509 name policy engine +func NewX509PolicyEngine(policyOptions X509PolicyOptionsInterface) (X509Policy, error) { + + // return early if no policy engine options to configure + if policyOptions == nil { + return nil, nil + } + + options := []policy.NamePolicyOption{} + + allowed := policyOptions.GetAllowedNameOptions() + if allowed != nil && allowed.HasNames() { + options = append(options, + policy.WithPermittedDNSDomains(allowed.DNSDomains), + policy.WithPermittedIPsOrCIDRs(allowed.IPRanges), + policy.WithPermittedEmailAddresses(allowed.EmailAddresses), + policy.WithPermittedURIDomains(allowed.URIDomains), + ) + } + + denied := policyOptions.GetDeniedNameOptions() + if denied != nil && denied.HasNames() { + options = append(options, + policy.WithExcludedDNSDomains(denied.DNSDomains), + policy.WithExcludedIPsOrCIDRs(denied.IPRanges), + policy.WithExcludedEmailAddresses(denied.EmailAddresses), + policy.WithExcludedURIDomains(denied.URIDomains), + ) + } + + // ensure no policy engine is returned when no name options were provided + if len(options) == 0 { + return nil, nil + } + + // enable x509 Subject Common Name validation by default + options = append(options, policy.WithSubjectCommonNameVerification()) + + return policy.New(options...) +} + +type sshPolicyEngineType string + +const ( + UserPolicyEngineType sshPolicyEngineType = "user" + HostPolicyEngineType sshPolicyEngineType = "host" +) + +// newSSHUserPolicyEngine creates a new SSH user certificate policy engine +func NewSSHUserPolicyEngine(policyOptions SSHPolicyOptionsInterface) (UserPolicy, error) { + policyEngine, err := newSSHPolicyEngine(policyOptions, UserPolicyEngineType) + if err != nil { + return nil, err + } + return policyEngine, nil +} + +// newSSHHostPolicyEngine create a new SSH host certificate policy engine +func NewSSHHostPolicyEngine(policyOptions SSHPolicyOptionsInterface) (HostPolicy, error) { + policyEngine, err := newSSHPolicyEngine(policyOptions, HostPolicyEngineType) + if err != nil { + return nil, err + } + return policyEngine, nil +} + +// newSSHPolicyEngine creates a new SSH name policy engine +func newSSHPolicyEngine(policyOptions SSHPolicyOptionsInterface, typ sshPolicyEngineType) (policy.SSHNamePolicyEngine, error) { + + // return early if no policy engine options to configure + if policyOptions == nil { + return nil, nil + } + + var ( + allowed *SSHNameOptions + denied *SSHNameOptions + ) + + switch typ { + case UserPolicyEngineType: + allowed = policyOptions.GetAllowedUserNameOptions() + denied = policyOptions.GetDeniedUserNameOptions() + case HostPolicyEngineType: + allowed = policyOptions.GetAllowedHostNameOptions() + denied = policyOptions.GetDeniedHostNameOptions() + default: + return nil, fmt.Errorf("unknown SSH policy engine type %s provided", typ) + } + + options := []policy.NamePolicyOption{} + + if allowed != nil && allowed.HasNames() { + options = append(options, + policy.WithPermittedDNSDomains(allowed.DNSDomains), + policy.WithPermittedIPsOrCIDRs(allowed.IPRanges), + policy.WithPermittedEmailAddresses(allowed.EmailAddresses), + policy.WithPermittedPrincipals(allowed.Principals), + ) + } + + if denied != nil && denied.HasNames() { + options = append(options, + policy.WithExcludedDNSDomains(denied.DNSDomains), + policy.WithExcludedIPsOrCIDRs(denied.IPRanges), + policy.WithExcludedEmailAddresses(denied.EmailAddresses), + policy.WithExcludedPrincipals(denied.Principals), + ) + } + + // ensure no policy engine is returned when no name options were provided + if len(options) == 0 { + return nil, nil + } + + return policy.New(options...) +} diff --git a/authority/provisioner/acme.go b/authority/provisioner/acme.go index 05d16e7f..2d5f74ff 100644 --- a/authority/provisioner/acme.go +++ b/authority/provisioner/acme.go @@ -7,8 +7,8 @@ import ( "time" "github.com/pkg/errors" + "github.com/smallstep/certificates/authority/policy" "github.com/smallstep/certificates/errs" - "github.com/smallstep/certificates/policy" ) // ACME is the acme provisioner type, an entity that can authorize the ACME @@ -27,7 +27,7 @@ type ACME struct { Claims *Claims `json:"claims,omitempty"` Options *Options `json:"options,omitempty"` claimer *Claimer - x509Policy policy.X509NamePolicyEngine + x509Policy policy.X509Policy } // GetID returns the provisioner unique identifier. @@ -92,7 +92,7 @@ func (p *ACME) Init(config Config) (err error) { // Initialize the x509 allow/deny policy engine // TODO(hs): ensure no race conditions happen when reloading settings and requesting certs? // TODO(hs): implement memoization strategy, so that reloading is not required when no changes were made to allow/deny? - if p.x509Policy, err = newX509PolicyEngine(p.Options.GetX509Options()); err != nil { + if p.x509Policy, err = policy.NewX509PolicyEngine(p.Options.GetX509Options()); err != nil { return err } diff --git a/authority/provisioner/aws.go b/authority/provisioner/aws.go index 2ff8ade9..81029b1d 100644 --- a/authority/provisioner/aws.go +++ b/authority/provisioner/aws.go @@ -17,6 +17,7 @@ import ( "time" "github.com/pkg/errors" + "github.com/smallstep/certificates/authority/policy" "github.com/smallstep/certificates/errs" "go.step.sm/crypto/jose" "go.step.sm/crypto/sshutil" @@ -267,8 +268,8 @@ type AWS struct { claimer *Claimer config *awsConfig audiences Audiences - x509Policy x509PolicyEngine - sshHostPolicy *hostPolicyEngine + x509Policy policy.X509Policy + sshHostPolicy policy.HostPolicy } // GetID returns the provisioner unique identifier. @@ -428,12 +429,12 @@ func (p *AWS) Init(config Config) (err error) { } // Initialize the x509 allow/deny policy engine - if p.x509Policy, err = newX509PolicyEngine(p.Options.GetX509Options()); err != nil { + if p.x509Policy, err = policy.NewX509PolicyEngine(p.Options.GetX509Options()); err != nil { return err } // Initialize the SSH allow/deny policy engine for host certificates - if p.sshHostPolicy, err = newSSHHostPolicyEngine(p.Options.GetSSHOptions()); err != nil { + if p.sshHostPolicy, err = policy.NewSSHHostPolicyEngine(p.Options.GetSSHOptions()); err != nil { return err } diff --git a/authority/provisioner/azure.go b/authority/provisioner/azure.go index f010364c..9c596b11 100644 --- a/authority/provisioner/azure.go +++ b/authority/provisioner/azure.go @@ -13,6 +13,7 @@ import ( "time" "github.com/pkg/errors" + "github.com/smallstep/certificates/authority/policy" "github.com/smallstep/certificates/errs" "go.step.sm/crypto/jose" "go.step.sm/crypto/sshutil" @@ -100,8 +101,8 @@ type Azure struct { config *azureConfig oidcConfig openIDConfiguration keyStore *keyStore - x509Policy x509PolicyEngine - sshHostPolicy *hostPolicyEngine + x509Policy policy.X509Policy + sshHostPolicy policy.HostPolicy } // GetID returns the provisioner unique identifier. @@ -226,12 +227,12 @@ func (p *Azure) Init(config Config) (err error) { } // Initialize the x509 allow/deny policy engine - if p.x509Policy, err = newX509PolicyEngine(p.Options.GetX509Options()); err != nil { + if p.x509Policy, err = policy.NewX509PolicyEngine(p.Options.GetX509Options()); err != nil { return err } // Initialize the SSH allow/deny policy engine for host certificates - if p.sshHostPolicy, err = newSSHHostPolicyEngine(p.Options.GetSSHOptions()); err != nil { + if p.sshHostPolicy, err = policy.NewSSHHostPolicyEngine(p.Options.GetSSHOptions()); err != nil { return err } diff --git a/authority/provisioner/gcp.go b/authority/provisioner/gcp.go index e56c0729..5f08f2f6 100644 --- a/authority/provisioner/gcp.go +++ b/authority/provisioner/gcp.go @@ -14,6 +14,7 @@ import ( "time" "github.com/pkg/errors" + "github.com/smallstep/certificates/authority/policy" "github.com/smallstep/certificates/errs" "go.step.sm/crypto/jose" "go.step.sm/crypto/sshutil" @@ -92,8 +93,8 @@ type GCP struct { config *gcpConfig keyStore *keyStore audiences Audiences - x509Policy x509PolicyEngine - sshHostPolicy *hostPolicyEngine + x509Policy policy.X509Policy + sshHostPolicy policy.HostPolicy } // GetID returns the provisioner unique identifier. The name should uniquely @@ -219,12 +220,12 @@ func (p *GCP) Init(config Config) error { } // Initialize the x509 allow/deny policy engine - if p.x509Policy, err = newX509PolicyEngine(p.Options.GetX509Options()); err != nil { + if p.x509Policy, err = policy.NewX509PolicyEngine(p.Options.GetX509Options()); err != nil { return err } // Initialize the SSH allow/deny policy engine for host certificates - if p.sshHostPolicy, err = newSSHHostPolicyEngine(p.Options.GetSSHOptions()); err != nil { + if p.sshHostPolicy, err = policy.NewSSHHostPolicyEngine(p.Options.GetSSHOptions()); err != nil { return err } diff --git a/authority/provisioner/jwk.go b/authority/provisioner/jwk.go index a129a536..b1716233 100644 --- a/authority/provisioner/jwk.go +++ b/authority/provisioner/jwk.go @@ -7,6 +7,7 @@ import ( "time" "github.com/pkg/errors" + "github.com/smallstep/certificates/authority/policy" "github.com/smallstep/certificates/errs" "go.step.sm/crypto/jose" "go.step.sm/crypto/sshutil" @@ -37,9 +38,9 @@ type JWK struct { Options *Options `json:"options,omitempty"` claimer *Claimer audiences Audiences - x509Policy x509PolicyEngine - sshHostPolicy *hostPolicyEngine - sshUserPolicy *userPolicyEngine + x509Policy policy.X509Policy + sshHostPolicy policy.HostPolicy + sshUserPolicy policy.UserPolicy } // GetID returns the provisioner unique identifier. The name and credential id @@ -107,17 +108,17 @@ func (p *JWK) Init(config Config) (err error) { } // Initialize the x509 allow/deny policy engine - if p.x509Policy, err = newX509PolicyEngine(p.Options.GetX509Options()); err != nil { + if p.x509Policy, err = policy.NewX509PolicyEngine(p.Options.GetX509Options()); err != nil { return err } // Initialize the SSH allow/deny policy engine for user certificates - if p.sshUserPolicy, err = newSSHUserPolicyEngine(p.Options.GetSSHOptions()); err != nil { + if p.sshUserPolicy, err = policy.NewSSHUserPolicyEngine(p.Options.GetSSHOptions()); err != nil { return err } // Initialize the SSH allow/deny policy engine for host certificates - if p.sshHostPolicy, err = newSSHHostPolicyEngine(p.Options.GetSSHOptions()); err != nil { + if p.sshHostPolicy, err = policy.NewSSHHostPolicyEngine(p.Options.GetSSHOptions()); err != nil { return err } diff --git a/authority/provisioner/k8sSA.go b/authority/provisioner/k8sSA.go index be55f114..7737c1cc 100644 --- a/authority/provisioner/k8sSA.go +++ b/authority/provisioner/k8sSA.go @@ -10,6 +10,7 @@ import ( "net/http" "github.com/pkg/errors" + "github.com/smallstep/certificates/authority/policy" "github.com/smallstep/certificates/errs" "go.step.sm/crypto/jose" "go.step.sm/crypto/pemutil" @@ -52,9 +53,9 @@ type K8sSA struct { audiences Audiences //kauthn kauthn.AuthenticationV1Interface pubKeys []interface{} - x509Policy x509PolicyEngine - sshHostPolicy *hostPolicyEngine - sshUserPolicy *userPolicyEngine + x509Policy policy.X509Policy + sshHostPolicy policy.HostPolicy + sshUserPolicy policy.UserPolicy } // GetID returns the provisioner unique identifier. The name and credential id @@ -148,17 +149,17 @@ func (p *K8sSA) Init(config Config) (err error) { } // Initialize the x509 allow/deny policy engine - if p.x509Policy, err = newX509PolicyEngine(p.Options.GetX509Options()); err != nil { + if p.x509Policy, err = policy.NewX509PolicyEngine(p.Options.GetX509Options()); err != nil { return err } // Initialize the SSH allow/deny policy engine for user certificates - if p.sshUserPolicy, err = newSSHUserPolicyEngine(p.Options.GetSSHOptions()); err != nil { + if p.sshUserPolicy, err = policy.NewSSHUserPolicyEngine(p.Options.GetSSHOptions()); err != nil { return err } // Initialize the SSH allow/deny policy engine for host certificates - if p.sshHostPolicy, err = newSSHHostPolicyEngine(p.Options.GetSSHOptions()); err != nil { + if p.sshHostPolicy, err = policy.NewSSHHostPolicyEngine(p.Options.GetSSHOptions()); err != nil { return err } diff --git a/authority/provisioner/nebula.go b/authority/provisioner/nebula.go index f8027de9..a9bfab9f 100644 --- a/authority/provisioner/nebula.go +++ b/authority/provisioner/nebula.go @@ -10,6 +10,7 @@ import ( "github.com/pkg/errors" nebula "github.com/slackhq/nebula/cert" + "github.com/smallstep/certificates/authority/policy" "github.com/smallstep/certificates/errs" "go.step.sm/crypto/jose" "go.step.sm/crypto/sshutil" @@ -43,9 +44,9 @@ type Nebula struct { claimer *Claimer caPool *nebula.NebulaCAPool audiences Audiences - x509Policy x509PolicyEngine - sshHostPolicy *hostPolicyEngine - sshUserPolicy *userPolicyEngine + x509Policy policy.X509Policy + sshHostPolicy policy.HostPolicy + sshUserPolicy policy.UserPolicy } // Init verifies and initializes the Nebula provisioner. @@ -72,17 +73,17 @@ func (p *Nebula) Init(config Config) error { p.audiences = config.Audiences.WithFragment(p.GetIDForToken()) // Initialize the x509 allow/deny policy engine - if p.x509Policy, err = newX509PolicyEngine(p.Options.GetX509Options()); err != nil { + if p.x509Policy, err = policy.NewX509PolicyEngine(p.Options.GetX509Options()); err != nil { return err } // Initialize the SSH allow/deny policy engine for user certificates - if p.sshUserPolicy, err = newSSHUserPolicyEngine(p.Options.GetSSHOptions()); err != nil { + if p.sshUserPolicy, err = policy.NewSSHUserPolicyEngine(p.Options.GetSSHOptions()); err != nil { return err } // Initialize the SSH allow/deny policy engine for host certificates - if p.sshHostPolicy, err = newSSHHostPolicyEngine(p.Options.GetSSHOptions()); err != nil { + if p.sshHostPolicy, err = policy.NewSSHHostPolicyEngine(p.Options.GetSSHOptions()); err != nil { return err } diff --git a/authority/provisioner/oidc.go b/authority/provisioner/oidc.go index 60bb5cf1..e3c8740a 100644 --- a/authority/provisioner/oidc.go +++ b/authority/provisioner/oidc.go @@ -12,6 +12,7 @@ import ( "time" "github.com/pkg/errors" + "github.com/smallstep/certificates/authority/policy" "github.com/smallstep/certificates/errs" "go.step.sm/crypto/jose" "go.step.sm/crypto/sshutil" @@ -94,9 +95,9 @@ type OIDC struct { keyStore *keyStore claimer *Claimer getIdentityFunc GetIdentityFunc - x509Policy x509PolicyEngine - sshHostPolicy *hostPolicyEngine - sshUserPolicy *userPolicyEngine + x509Policy policy.X509Policy + sshHostPolicy policy.HostPolicy + sshUserPolicy policy.UserPolicy } func sanitizeEmail(email string) string { @@ -212,17 +213,17 @@ func (o *OIDC) Init(config Config) (err error) { } // Initialize the x509 allow/deny policy engine - if o.x509Policy, err = newX509PolicyEngine(o.Options.GetX509Options()); err != nil { + if o.x509Policy, err = policy.NewX509PolicyEngine(o.Options.GetX509Options()); err != nil { return err } // Initialize the SSH allow/deny policy engine for user certificates - if o.sshUserPolicy, err = newSSHUserPolicyEngine(o.Options.GetSSHOptions()); err != nil { + if o.sshUserPolicy, err = policy.NewSSHUserPolicyEngine(o.Options.GetSSHOptions()); err != nil { return err } // Initialize the SSH allow/deny policy engine for host certificates - if o.sshHostPolicy, err = newSSHHostPolicyEngine(o.Options.GetSSHOptions()); err != nil { + if o.sshHostPolicy, err = policy.NewSSHHostPolicyEngine(o.Options.GetSSHOptions()); err != nil { return err } diff --git a/authority/provisioner/options.go b/authority/provisioner/options.go index 257a2107..7725c8b0 100644 --- a/authority/provisioner/options.go +++ b/authority/provisioner/options.go @@ -5,8 +5,11 @@ import ( "strings" "github.com/pkg/errors" + "go.step.sm/crypto/jose" "go.step.sm/crypto/x509util" + + "github.com/smallstep/certificates/authority/policy" ) // CertificateOptions is an interface that returns a list of options passed when @@ -58,10 +61,10 @@ type X509Options struct { TemplateData json.RawMessage `json:"templateData,omitempty"` // AllowedNames contains the SANs the provisioner is authorized to sign - AllowedNames *X509NameOptions `json:"allow,omitempty"` + AllowedNames *policy.X509NameOptions // DeniedNames contains the SANs the provisioner is not authorized to sign - DeniedNames *X509NameOptions `json:"deny,omitempty"` + DeniedNames *policy.X509NameOptions } // HasTemplate returns true if a template is defined in the provisioner options. @@ -69,41 +72,24 @@ func (o *X509Options) HasTemplate() bool { return o != nil && (o.Template != "" || o.TemplateFile != "") } -// GetAllowedNameOptions returns the AllowedNameOptions, which models the +// GetAllowedNameOptions returns the AllowedNames, which models the // SANs that a provisioner is authorized to sign x509 certificates for. -func (o *X509Options) GetAllowedNameOptions() *X509NameOptions { +func (o *X509Options) GetAllowedNameOptions() *policy.X509NameOptions { if o == nil { return nil } return o.AllowedNames } -// GetDeniedNameOptions returns the DeniedNameOptions, which models the +// GetDeniedNameOptions returns the DeniedNames, which models the // SANs that a provisioner is NOT authorized to sign x509 certificates for. -func (o *X509Options) GetDeniedNameOptions() *X509NameOptions { +func (o *X509Options) GetDeniedNameOptions() *policy.X509NameOptions { if o == nil { return nil } return o.DeniedNames } -// X509NameOptions models the X509 name policy configuration. -type X509NameOptions struct { - DNSDomains []string `json:"dns,omitempty"` - IPRanges []string `json:"ip,omitempty"` - EmailAddresses []string `json:"email,omitempty"` - URIDomains []string `json:"uri,omitempty"` -} - -// HasNames checks if the AllowedNameOptions has one or more -// names configured. -func (o *X509NameOptions) HasNames() bool { - return len(o.DNSDomains) > 0 || - len(o.IPRanges) > 0 || - len(o.EmailAddresses) > 0 || - len(o.URIDomains) > 0 -} - // 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. If no template has been provided, diff --git a/authority/provisioner/policy.go b/authority/provisioner/policy.go deleted file mode 100644 index b9740e39..00000000 --- a/authority/provisioner/policy.go +++ /dev/null @@ -1,156 +0,0 @@ -package provisioner - -import ( - "fmt" - - "github.com/smallstep/certificates/policy" - "golang.org/x/crypto/ssh" -) - -type sshPolicyEngineType string - -const ( - userPolicyEngineType sshPolicyEngineType = "user" - hostPolicyEngineType sshPolicyEngineType = "host" -) - -var certTypeToPolicyEngineType = map[uint32]sshPolicyEngineType{ - uint32(ssh.UserCert): userPolicyEngineType, - uint32(ssh.HostCert): hostPolicyEngineType, -} - -type x509PolicyEngine interface { - policy.X509NamePolicyEngine -} - -type userPolicyEngine struct { - policy.SSHNamePolicyEngine -} - -type hostPolicyEngine struct { - policy.SSHNamePolicyEngine -} - -// newX509PolicyEngine creates a new x509 name policy engine -func newX509PolicyEngine(x509Opts *X509Options) (x509PolicyEngine, error) { - - if x509Opts == nil { - return nil, nil - } - - options := []policy.NamePolicyOption{ - policy.WithSubjectCommonNameVerification(), // enable x509 Subject Common Name validation by default - } - - allowed := x509Opts.GetAllowedNameOptions() - if allowed != nil && allowed.HasNames() { - options = append(options, - policy.WithPermittedDNSDomains(allowed.DNSDomains), - policy.WithPermittedIPsOrCIDRs(allowed.IPRanges), - policy.WithPermittedEmailAddresses(allowed.EmailAddresses), - policy.WithPermittedURIDomains(allowed.URIDomains), - ) - } - - denied := x509Opts.GetDeniedNameOptions() - if denied != nil && denied.HasNames() { - options = append(options, - policy.WithExcludedDNSDomains(denied.DNSDomains), - policy.WithExcludedIPsOrCIDRs(denied.IPRanges), - policy.WithExcludedEmailAddresses(denied.EmailAddresses), - policy.WithExcludedURIDomains(denied.URIDomains), - ) - } - - return policy.New(options...) -} - -// newSSHUserPolicyEngine creates a new SSH user certificate policy engine -func newSSHUserPolicyEngine(sshOpts *SSHOptions) (*userPolicyEngine, error) { - policyEngine, err := newSSHPolicyEngine(sshOpts, userPolicyEngineType) - if err != nil { - return nil, err - } - // ensure we're not wrapping a nil engine - if policyEngine == nil { - return nil, nil - } - return &userPolicyEngine{ - SSHNamePolicyEngine: policyEngine, - }, nil -} - -// newSSHHostPolicyEngine create a new SSH host certificate policy engine -func newSSHHostPolicyEngine(sshOpts *SSHOptions) (*hostPolicyEngine, error) { - policyEngine, err := newSSHPolicyEngine(sshOpts, hostPolicyEngineType) - if err != nil { - return nil, err - } - // ensure we're not wrapping a nil engine - if policyEngine == nil { - return nil, nil - } - return &hostPolicyEngine{ - SSHNamePolicyEngine: policyEngine, - }, nil -} - -// newSSHPolicyEngine creates a new SSH name policy engine -func newSSHPolicyEngine(sshOpts *SSHOptions, typ sshPolicyEngineType) (policy.SSHNamePolicyEngine, error) { - - if sshOpts == nil { - return nil, nil - } - - var ( - allowed *SSHNameOptions - denied *SSHNameOptions - ) - - // TODO: embed the type in the policy engine itself for reference? - switch typ { - case userPolicyEngineType: - if sshOpts.User != nil { - allowed = sshOpts.User.GetAllowedNameOptions() - denied = sshOpts.User.GetDeniedNameOptions() - } - case hostPolicyEngineType: - if sshOpts.Host != nil { - allowed = sshOpts.Host.AllowedNames - denied = sshOpts.Host.DeniedNames - } - default: - return nil, fmt.Errorf("unknown SSH policy engine type %s provided", typ) - } - - options := []policy.NamePolicyOption{} - - if allowed != nil && allowed.HasNames() { - options = append(options, - policy.WithPermittedDNSDomains(allowed.DNSDomains), - policy.WithPermittedIPsOrCIDRs(allowed.IPRanges), - policy.WithPermittedEmailAddresses(allowed.EmailAddresses), - policy.WithPermittedPrincipals(allowed.Principals), - ) - } - - if denied != nil && denied.HasNames() { - options = append(options, - policy.WithExcludedDNSDomains(denied.DNSDomains), - policy.WithExcludedIPsOrCIDRs(denied.IPRanges), - policy.WithExcludedEmailAddresses(denied.EmailAddresses), - policy.WithExcludedPrincipals(denied.Principals), - ) - } - - // Return nil, because there's no policy to execute. This is - // important, because the logic that determines user vs. host certs - // are allowed depends on this fact. The two policy engines are - // not aware of eachother, so this check is performed in the - // SSH name validator, instead. - if len(options) == 0 { - return nil, nil - } - - return policy.New(options...) -} diff --git a/authority/provisioner/scep.go b/authority/provisioner/scep.go index a9a06cae..9d02aebb 100644 --- a/authority/provisioner/scep.go +++ b/authority/provisioner/scep.go @@ -5,7 +5,7 @@ import ( "time" "github.com/pkg/errors" - "github.com/smallstep/certificates/policy" + "github.com/smallstep/certificates/authority/policy" ) // SCEP is the SCEP provisioner type, an entity that can authorize the @@ -31,7 +31,7 @@ type SCEP struct { Options *Options `json:"options,omitempty"` Claims *Claims `json:"claims,omitempty"` claimer *Claimer - x509Policy policy.X509NamePolicyEngine + x509Policy policy.X509Policy secretChallengePassword string encryptionAlgorithm int } @@ -116,7 +116,7 @@ func (s *SCEP) Init(config Config) (err error) { // TODO: add other, SCEP specific, options? // Initialize the x509 allow/deny policy engine - if s.x509Policy, err = newX509PolicyEngine(s.Options.GetX509Options()); err != nil { + if s.x509Policy, err = policy.NewX509PolicyEngine(s.Options.GetX509Options()); err != nil { return err } diff --git a/authority/provisioner/sign_options.go b/authority/provisioner/sign_options.go index 3327310b..082d765d 100644 --- a/authority/provisioner/sign_options.go +++ b/authority/provisioner/sign_options.go @@ -15,6 +15,7 @@ import ( "time" "github.com/pkg/errors" + "github.com/smallstep/certificates/authority/policy" "github.com/smallstep/certificates/errs" "go.step.sm/crypto/keyutil" "go.step.sm/crypto/x509util" @@ -407,18 +408,17 @@ func (v *validityValidator) Valid(cert *x509.Certificate, o SignOptions) error { // x509NamePolicyValidator validates that the certificate (to be signed) // contains only allowed SANs. type x509NamePolicyValidator struct { - policyEngine x509PolicyEngine + policyEngine policy.X509Policy } // newX509NamePolicyValidator return a new SANs allow/deny validator. -func newX509NamePolicyValidator(engine x509PolicyEngine) *x509NamePolicyValidator { +func newX509NamePolicyValidator(engine policy.X509Policy) *x509NamePolicyValidator { return &x509NamePolicyValidator{ policyEngine: engine, } } -// Valid validates validates that the certificate (to be signed) -// contains only allowed SANs. +// Valid validates that the certificate (to be signed) contains only allowed SANs. func (v *x509NamePolicyValidator) Valid(cert *x509.Certificate, _ SignOptions) error { if v.policyEngine == nil { return nil diff --git a/authority/provisioner/sign_ssh_options.go b/authority/provisioner/sign_ssh_options.go index 8f9cf466..a057b2b9 100644 --- a/authority/provisioner/sign_ssh_options.go +++ b/authority/provisioner/sign_ssh_options.go @@ -10,6 +10,7 @@ import ( "time" "github.com/pkg/errors" + "github.com/smallstep/certificates/authority/policy" "github.com/smallstep/certificates/errs" "go.step.sm/crypto/keyutil" "golang.org/x/crypto/ssh" @@ -448,20 +449,19 @@ func (v sshDefaultPublicKeyValidator) Valid(cert *ssh.Certificate, o SignSSHOpti // sshNamePolicyValidator validates that the certificate (to be signed) // contains only allowed principals. type sshNamePolicyValidator struct { - hostPolicyEngine *hostPolicyEngine - userPolicyEngine *userPolicyEngine + hostPolicyEngine policy.HostPolicy + userPolicyEngine policy.UserPolicy } // newSSHNamePolicyValidator return a new SSH allow/deny validator. -func newSSHNamePolicyValidator(host *hostPolicyEngine, user *userPolicyEngine) *sshNamePolicyValidator { +func newSSHNamePolicyValidator(host policy.HostPolicy, user policy.UserPolicy) *sshNamePolicyValidator { return &sshNamePolicyValidator{ hostPolicyEngine: host, userPolicyEngine: user, } } -// Valid validates validates that the certificate (to be signed) -// contains only allowed principals. +// Valid validates that the certificate (to be signed) contains only allowed principals. func (v *sshNamePolicyValidator) Valid(cert *ssh.Certificate, _ SignSSHOptions) error { if v.hostPolicyEngine == nil && v.userPolicyEngine == nil { // no policy configured at all; allow anything @@ -473,29 +473,25 @@ func (v *sshNamePolicyValidator) Valid(cert *ssh.Certificate, _ SignSSHOptions) // the same for host certs: if only a user policy engine is configured, host // certs are denied. When both policy engines are configured, the type of // cert determines which policy engine is used. - policyType, ok := certTypeToPolicyEngineType[cert.CertType] - if !ok { - return fmt.Errorf("unexpected SSH cert type %d", cert.CertType) - } - switch policyType { - case hostPolicyEngineType: + switch cert.CertType { + case ssh.HostCert: // when no host policy engine is configured, but a user policy engine is - // configured, we don't allow the host certificate. + // configured, the host certificate is denied. if v.hostPolicyEngine == nil && v.userPolicyEngine != nil { - return errors.New("SSH host certificate not authorized") // TODO: include principals in message? + return errors.New("SSH host certificate not authorized") } _, err := v.hostPolicyEngine.ArePrincipalsAllowed(cert) return err - case userPolicyEngineType: + case ssh.UserCert: // when no user policy engine is configured, but a host policy engine is - // configured, we don't allow the user certificate. + // configured, the user certificate is denied. if v.userPolicyEngine == nil && v.hostPolicyEngine != nil { - return errors.New("SSH user certificate not authorized") // TODO: include principals in message? + return errors.New("SSH user certificate not authorized") } _, err := v.userPolicyEngine.ArePrincipalsAllowed(cert) return err default: - return fmt.Errorf("unexpected policy engine type %q", policyType) // satisfy return; shouldn't happen + return fmt.Errorf("unexpected SSH certificate type %d", cert.CertType) // satisfy return; shouldn't happen } } diff --git a/authority/provisioner/ssh_options.go b/authority/provisioner/ssh_options.go index dacafc80..92c5826b 100644 --- a/authority/provisioner/ssh_options.go +++ b/authority/provisioner/ssh_options.go @@ -6,6 +6,8 @@ import ( "github.com/pkg/errors" "go.step.sm/crypto/sshutil" + + "github.com/smallstep/certificates/authority/policy" ) // SSHCertificateOptions is an interface that returns a list of options passed when @@ -35,32 +37,58 @@ type SSHOptions struct { TemplateData json.RawMessage `json:"templateData,omitempty"` // User contains SSH user certificate options. - User *SSHUserCertificateOptions `json:"user,omitempty"` + User *policy.SSHUserCertificateOptions // Host contains SSH host certificate options. - Host *SSHHostCertificateOptions `json:"host,omitempty"` + Host *policy.SSHHostCertificateOptions } -// SSHUserCertificateOptions is a collection of SSH user certificate options. -type SSHUserCertificateOptions struct { - // AllowedNames contains the names the provisioner is authorized to sign - AllowedNames *SSHNameOptions `json:"allow,omitempty"` - - // DeniedNames contains the names the provisioner is not authorized to sign - DeniedNames *SSHNameOptions `json:"deny,omitempty"` +// GetAllowedUserNameOptions returns the SSHNameOptions that are +// allowed when SSH User certificates are requested. +func (o *SSHOptions) GetAllowedUserNameOptions() *policy.SSHNameOptions { + if o == nil { + return nil + } + if o.User == nil { + return nil + } + return o.User.AllowedNames } -// SSHHostCertificateOptions is a collection of SSH host certificate options. -// It's an alias of SSHUserCertificateOptions, as the options are the same -// for both types of certificates. -type SSHHostCertificateOptions SSHUserCertificateOptions +// GetDeniedUserNameOptions returns the SSHNameOptions that are +// denied when SSH user certificates are requested. +func (o *SSHOptions) GetDeniedUserNameOptions() *policy.SSHNameOptions { + if o == nil { + return nil + } + if o.User == nil { + return nil + } + return o.User.DeniedNames +} -// SSHNameOptions models the SSH name policy configuration. -type SSHNameOptions struct { - DNSDomains []string `json:"dns,omitempty"` - IPRanges []string `json:"ip,omitempty"` - EmailAddresses []string `json:"email,omitempty"` - Principals []string `json:"principal,omitempty"` +// GetAllowedHostNameOptions returns the SSHNameOptions that are +// allowed when SSH host certificates are requested. +func (o *SSHOptions) GetAllowedHostNameOptions() *policy.SSHNameOptions { + if o == nil { + return nil + } + if o.Host == nil { + return nil + } + return o.Host.AllowedNames +} + +// GetDeniedHostNameOptions returns the SSHNameOptions that are +// denied when SSH host certificates are requested. +func (o *SSHOptions) GetDeniedHostNameOptions() *policy.SSHNameOptions { + if o == nil { + return nil + } + if o.Host == nil { + return nil + } + return o.Host.DeniedNames } // HasTemplate returns true if a template is defined in the provisioner options. @@ -68,32 +96,6 @@ func (o *SSHOptions) HasTemplate() bool { return o != nil && (o.Template != "" || o.TemplateFile != "") } -// GetAllowedNameOptions returns the AllowedSSHNameOptions, which models the -// names that a provisioner is authorized to sign SSH certificates for. -func (o *SSHUserCertificateOptions) GetAllowedNameOptions() *SSHNameOptions { - if o == nil { - return nil - } - return o.AllowedNames -} - -// GetDeniedNameOptions returns the DeniedSSHNameOptions, which models the -// names that a provisioner is NOT authorized to sign SSH certificates for. -func (o *SSHUserCertificateOptions) GetDeniedNameOptions() *SSHNameOptions { - if o == nil { - return nil - } - return o.DeniedNames -} - -// HasNames checks if the SSHNameOptions has one or more -// names configured. -func (o *SSHNameOptions) HasNames() bool { - return len(o.DNSDomains) > 0 || - len(o.EmailAddresses) > 0 || - len(o.Principals) > 0 -} - // TemplateSSHOptions generates a SSHCertificateOptions with the template and // data defined in the ProvisionerOptions, the provisioner generated data, and // the user data provided in the request. If no template has been provided, diff --git a/authority/provisioner/x5c.go b/authority/provisioner/x5c.go index 12112cc6..a8275474 100644 --- a/authority/provisioner/x5c.go +++ b/authority/provisioner/x5c.go @@ -8,6 +8,7 @@ import ( "time" "github.com/pkg/errors" + "github.com/smallstep/certificates/authority/policy" "github.com/smallstep/certificates/errs" "go.step.sm/crypto/jose" "go.step.sm/crypto/sshutil" @@ -35,9 +36,9 @@ type X5C struct { claimer *Claimer audiences Audiences rootPool *x509.CertPool - x509Policy x509PolicyEngine - sshHostPolicy *hostPolicyEngine - sshUserPolicy *userPolicyEngine + x509Policy policy.X509Policy + sshHostPolicy policy.HostPolicy + sshUserPolicy policy.UserPolicy } // GetID returns the provisioner unique identifier. The name and credential id @@ -129,17 +130,17 @@ func (p *X5C) Init(config Config) error { } // Initialize the x509 allow/deny policy engine - if p.x509Policy, err = newX509PolicyEngine(p.Options.GetX509Options()); err != nil { + if p.x509Policy, err = policy.NewX509PolicyEngine(p.Options.GetX509Options()); err != nil { return err } // Initialize the SSH allow/deny policy engine for user certificates - if p.sshUserPolicy, err = newSSHUserPolicyEngine(p.Options.GetSSHOptions()); err != nil { + if p.sshUserPolicy, err = policy.NewSSHUserPolicyEngine(p.Options.GetSSHOptions()); err != nil { return err } // Initialize the SSH allow/deny policy engine for host certificates - if p.sshHostPolicy, err = newSSHHostPolicyEngine(p.Options.GetSSHOptions()); err != nil { + if p.sshHostPolicy, err = policy.NewSSHHostPolicyEngine(p.Options.GetSSHOptions()); err != nil { return err } diff --git a/authority/ssh.go b/authority/ssh.go index 4a67b28c..7c3df192 100644 --- a/authority/ssh.go +++ b/authority/ssh.go @@ -9,6 +9,7 @@ import ( "strings" "time" + "github.com/pkg/errors" "github.com/smallstep/certificates/authority/config" "github.com/smallstep/certificates/authority/provisioner" "github.com/smallstep/certificates/db" @@ -241,6 +242,45 @@ func (a *Authority) SignSSH(ctx context.Context, key ssh.PublicKey, opts provisi return nil, errs.InternalServer("authority.SignSSH: unexpected ssh certificate type: %d", certTpl.CertType) } + switch certTpl.CertType { + case ssh.UserCert: + // when no user policy engine is configured, but a host policy engine is + // configured, the user certificate is denied. + if a.sshUserPolicy == nil && a.sshHostPolicy != nil { + return nil, errs.ForbiddenErr(errors.New("authority not allowed to sign ssh user certificates"), "authority.SignSSH: error creating ssh user certificate") + } + if a.sshUserPolicy != nil { + allowed, err := a.sshUserPolicy.ArePrincipalsAllowed(certTpl) + if err != nil { + return nil, errs.InternalServerErr(err, + errs.WithMessage("authority.SignSSH: error creating ssh user certificate"), + ) + } + if !allowed { + return nil, errs.ForbiddenErr(errors.New("authority not allowed to sign"), "authority.SignSSH: error creating ssh user certificate") + } + } + case ssh.HostCert: + // when no host policy engine is configured, but a user policy engine is + // configured, the host certificate is denied. + if a.sshHostPolicy == nil && a.sshUserPolicy != nil { + return nil, errs.ForbiddenErr(errors.New("authority not allowed to sign ssh host certificates"), "authority.SignSSH: error creating ssh user certificate") + } + if a.sshHostPolicy != nil { + allowed, err := a.sshHostPolicy.ArePrincipalsAllowed(certTpl) + if err != nil { + return nil, errs.InternalServerErr(err, + errs.WithMessage("authority.SignSSH: error creating ssh host certificate"), + ) + } + if !allowed { + return nil, errs.ForbiddenErr(errors.New("authority not allowed to sign"), "authority.SignSSH: error creating ssh host certificate") + } + } + default: + return nil, errs.InternalServer("authority.SignSSH: unexpected ssh certificate type: %d", certTpl.CertType) + } + // Sign certificate. cert, err := sshutil.CreateCertificate(certTpl, signer) if err != nil { diff --git a/authority/tls.go b/authority/tls.go index 58a1247c..d749e2ad 100644 --- a/authority/tls.go +++ b/authority/tls.go @@ -191,6 +191,25 @@ func (a *Authority) Sign(csr *x509.CertificateRequest, signOpts provisioner.Sign } } + // If a policy is configured, perform allow/deny policy check on authority level + if a.x509Policy != nil { + allowed, err := a.x509Policy.AreCertificateNamesAllowed(leaf) + if err != nil { + return nil, errs.InternalServerErr(err, + errs.WithKeyVal("csr", csr), + errs.WithKeyVal("signOptions", signOpts), + errs.WithMessage("error creating certificate"), + ) + } + if !allowed { + // TODO: include SANs in error message? + return nil, errs.ApplyOptions( + errs.ForbiddenErr(errors.New("authority not allowed to sign"), "error creating certificate"), + opts..., + ) + } + } + // Sign certificate lifetime := leaf.NotAfter.Sub(leaf.NotBefore.Add(signOpts.Backdate)) resp, err := a.x509CAService.CreateCertificate(&casapi.CreateCertificateRequest{