Refactor configuration of allow/deny on authority level

This commit is contained in:
Herman Slatman 2022-03-08 13:26:07 +01:00
parent af53a17bb4
commit 7c541888ad
No known key found for this signature in database
GPG key ID: F4D8A44EA0A75A4F
21 changed files with 515 additions and 293 deletions

View file

@ -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

View file

@ -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"`

170
authority/policy/options.go Normal file
View file

@ -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
}

134
authority/policy/policy.go Normal file
View file

@ -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...)
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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,

View file

@ -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...)
}

View file

@ -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
}

View file

@ -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

View file

@ -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
}
}

View file

@ -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,

View file

@ -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
}

View file

@ -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 {

View file

@ -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{