Split SSH user and cert policy configuration and execution
This commit is contained in:
parent
a7eb27d309
commit
88c7b63c9d
16 changed files with 285 additions and 139 deletions
|
@ -18,7 +18,6 @@ import (
|
|||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/smallstep/certificates/errs"
|
||||
"github.com/smallstep/certificates/policy"
|
||||
"go.step.sm/crypto/jose"
|
||||
"go.step.sm/crypto/sshutil"
|
||||
"go.step.sm/crypto/x509util"
|
||||
|
@ -268,8 +267,8 @@ type AWS struct {
|
|||
claimer *Claimer
|
||||
config *awsConfig
|
||||
audiences Audiences
|
||||
x509Policy policy.X509NamePolicyEngine
|
||||
sshPolicy policy.SSHNamePolicyEngine
|
||||
x509Policy x509PolicyEngine
|
||||
sshHostPolicy *hostPolicyEngine
|
||||
}
|
||||
|
||||
// GetID returns the provisioner unique identifier.
|
||||
|
@ -433,8 +432,8 @@ func (p *AWS) Init(config Config) (err error) {
|
|||
return err
|
||||
}
|
||||
|
||||
// Initialize the SSH allow/deny policy engine
|
||||
if p.sshPolicy, err = newSSHPolicyEngine(p.Options.GetSSHOptions()); err != nil {
|
||||
// Initialize the SSH allow/deny policy engine for host certificates
|
||||
if p.sshHostPolicy, err = newSSHHostPolicyEngine(p.Options.GetSSHOptions()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -774,6 +773,6 @@ func (p *AWS) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption,
|
|||
// Require all the fields in the SSH certificate
|
||||
&sshCertDefaultValidator{},
|
||||
// Ensure that all principal names are allowed
|
||||
newSSHNamePolicyValidator(p.sshPolicy),
|
||||
newSSHNamePolicyValidator(p.sshHostPolicy, nil),
|
||||
), nil
|
||||
}
|
||||
|
|
|
@ -14,7 +14,6 @@ import (
|
|||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/smallstep/certificates/errs"
|
||||
"github.com/smallstep/certificates/policy"
|
||||
"go.step.sm/crypto/jose"
|
||||
"go.step.sm/crypto/sshutil"
|
||||
"go.step.sm/crypto/x509util"
|
||||
|
@ -99,8 +98,8 @@ type Azure struct {
|
|||
config *azureConfig
|
||||
oidcConfig openIDConfiguration
|
||||
keyStore *keyStore
|
||||
x509Policy policy.X509NamePolicyEngine
|
||||
sshPolicy policy.SSHNamePolicyEngine
|
||||
x509Policy x509PolicyEngine
|
||||
sshHostPolicy *hostPolicyEngine
|
||||
}
|
||||
|
||||
// GetID returns the provisioner unique identifier.
|
||||
|
@ -229,8 +228,8 @@ func (p *Azure) Init(config Config) (err error) {
|
|||
return err
|
||||
}
|
||||
|
||||
// Initialize the SSH allow/deny policy engine
|
||||
if p.sshPolicy, err = newSSHPolicyEngine(p.Options.GetSSHOptions()); err != nil {
|
||||
// Initialize the SSH allow/deny policy engine for host certificates
|
||||
if p.sshHostPolicy, err = newSSHHostPolicyEngine(p.Options.GetSSHOptions()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -411,7 +410,7 @@ func (p *Azure) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOptio
|
|||
// Require all the fields in the SSH certificate
|
||||
&sshCertDefaultValidator{},
|
||||
// Ensure that all principal names are allowed
|
||||
newSSHNamePolicyValidator(p.sshPolicy),
|
||||
newSSHNamePolicyValidator(p.sshHostPolicy, nil),
|
||||
), nil
|
||||
}
|
||||
|
||||
|
|
|
@ -15,7 +15,6 @@ import (
|
|||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/smallstep/certificates/errs"
|
||||
"github.com/smallstep/certificates/policy"
|
||||
"go.step.sm/crypto/jose"
|
||||
"go.step.sm/crypto/sshutil"
|
||||
"go.step.sm/crypto/x509util"
|
||||
|
@ -93,8 +92,8 @@ type GCP struct {
|
|||
config *gcpConfig
|
||||
keyStore *keyStore
|
||||
audiences Audiences
|
||||
x509Policy policy.X509NamePolicyEngine
|
||||
sshPolicy policy.SSHNamePolicyEngine
|
||||
x509Policy x509PolicyEngine
|
||||
sshHostPolicy *hostPolicyEngine
|
||||
}
|
||||
|
||||
// GetID returns the provisioner unique identifier. The name should uniquely
|
||||
|
@ -224,8 +223,8 @@ func (p *GCP) Init(config Config) error {
|
|||
return err
|
||||
}
|
||||
|
||||
// Initialize the SSH allow/deny policy engine
|
||||
if p.sshPolicy, err = newSSHPolicyEngine(p.Options.GetSSHOptions()); err != nil {
|
||||
// Initialize the SSH allow/deny policy engine for host certificates
|
||||
if p.sshHostPolicy, err = newSSHHostPolicyEngine(p.Options.GetSSHOptions()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -453,6 +452,6 @@ func (p *GCP) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption,
|
|||
// Require all the fields in the SSH certificate
|
||||
&sshCertDefaultValidator{},
|
||||
// Ensure that all principal names are allowed
|
||||
newSSHNamePolicyValidator(p.sshPolicy),
|
||||
newSSHNamePolicyValidator(p.sshHostPolicy, nil),
|
||||
), nil
|
||||
}
|
||||
|
|
|
@ -8,7 +8,6 @@ import (
|
|||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/smallstep/certificates/errs"
|
||||
"github.com/smallstep/certificates/policy"
|
||||
"go.step.sm/crypto/jose"
|
||||
"go.step.sm/crypto/sshutil"
|
||||
"go.step.sm/crypto/x509util"
|
||||
|
@ -38,8 +37,9 @@ type JWK struct {
|
|||
Options *Options `json:"options,omitempty"`
|
||||
claimer *Claimer
|
||||
audiences Audiences
|
||||
x509Policy policy.X509NamePolicyEngine
|
||||
sshPolicy policy.SSHNamePolicyEngine
|
||||
x509Policy x509PolicyEngine
|
||||
sshHostPolicy *hostPolicyEngine
|
||||
sshUserPolicy *userPolicyEngine
|
||||
}
|
||||
|
||||
// GetID returns the provisioner unique identifier. The name and credential id
|
||||
|
@ -111,8 +111,13 @@ func (p *JWK) Init(config Config) (err error) {
|
|||
return err
|
||||
}
|
||||
|
||||
// Initialize the SSH allow/deny policy engine
|
||||
if p.sshPolicy, err = newSSHPolicyEngine(p.Options.GetSSHOptions()); err != nil {
|
||||
// Initialize the SSH allow/deny policy engine for user certificates
|
||||
if p.sshUserPolicy, err = 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 {
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -294,7 +299,7 @@ func (p *JWK) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption,
|
|||
// Require and validate all the default fields in the SSH certificate.
|
||||
&sshCertDefaultValidator{},
|
||||
// Ensure that all principal names are allowed
|
||||
newSSHNamePolicyValidator(p.sshPolicy),
|
||||
newSSHNamePolicyValidator(p.sshHostPolicy, p.sshUserPolicy),
|
||||
), nil
|
||||
}
|
||||
|
||||
|
|
|
@ -11,7 +11,6 @@ import (
|
|||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/smallstep/certificates/errs"
|
||||
"github.com/smallstep/certificates/policy"
|
||||
"go.step.sm/crypto/jose"
|
||||
"go.step.sm/crypto/pemutil"
|
||||
"go.step.sm/crypto/sshutil"
|
||||
|
@ -53,8 +52,9 @@ type K8sSA struct {
|
|||
audiences Audiences
|
||||
//kauthn kauthn.AuthenticationV1Interface
|
||||
pubKeys []interface{}
|
||||
x509Policy policy.X509NamePolicyEngine
|
||||
sshPolicy policy.SSHNamePolicyEngine
|
||||
x509Policy x509PolicyEngine
|
||||
sshHostPolicy *hostPolicyEngine
|
||||
sshUserPolicy *userPolicyEngine
|
||||
}
|
||||
|
||||
// GetID returns the provisioner unique identifier. The name and credential id
|
||||
|
@ -152,8 +152,13 @@ func (p *K8sSA) Init(config Config) (err error) {
|
|||
return err
|
||||
}
|
||||
|
||||
// Initialize the SSH allow/deny policy engine
|
||||
if p.sshPolicy, err = newSSHPolicyEngine(p.Options.GetSSHOptions()); err != nil {
|
||||
// Initialize the SSH allow/deny policy engine for user certificates
|
||||
if p.sshUserPolicy, err = 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 {
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -305,7 +310,7 @@ func (p *K8sSA) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOptio
|
|||
// Require and validate all the default fields in the SSH certificate.
|
||||
&sshCertDefaultValidator{},
|
||||
// Ensure that all principal names are allowed
|
||||
newSSHNamePolicyValidator(p.sshPolicy),
|
||||
newSSHNamePolicyValidator(p.sshHostPolicy, p.sshUserPolicy),
|
||||
), nil
|
||||
}
|
||||
|
||||
|
|
|
@ -371,7 +371,8 @@ func TestK8sSA_AuthorizeSSHSign(t *testing.T) {
|
|||
case *sshDefaultDuration:
|
||||
assert.Equals(t, v.Claimer, tc.p.claimer)
|
||||
case *sshNamePolicyValidator:
|
||||
assert.Equals(t, nil, v.policyEngine)
|
||||
assert.Equals(t, nil, v.userPolicyEngine)
|
||||
assert.Equals(t, nil, v.hostPolicyEngine)
|
||||
default:
|
||||
assert.FatalError(t, errors.Errorf("unexpected sign option of type %T", v))
|
||||
}
|
||||
|
|
|
@ -11,7 +11,6 @@ import (
|
|||
"github.com/pkg/errors"
|
||||
nebula "github.com/slackhq/nebula/cert"
|
||||
"github.com/smallstep/certificates/errs"
|
||||
"github.com/smallstep/certificates/policy"
|
||||
"go.step.sm/crypto/jose"
|
||||
"go.step.sm/crypto/sshutil"
|
||||
"go.step.sm/crypto/x25519"
|
||||
|
@ -44,8 +43,9 @@ type Nebula struct {
|
|||
claimer *Claimer
|
||||
caPool *nebula.NebulaCAPool
|
||||
audiences Audiences
|
||||
x509Policy policy.X509NamePolicyEngine
|
||||
sshPolicy policy.SSHNamePolicyEngine
|
||||
x509Policy x509PolicyEngine
|
||||
sshHostPolicy *hostPolicyEngine
|
||||
sshUserPolicy *userPolicyEngine
|
||||
}
|
||||
|
||||
// Init verifies and initializes the Nebula provisioner.
|
||||
|
@ -76,8 +76,13 @@ func (p *Nebula) Init(config Config) error {
|
|||
return err
|
||||
}
|
||||
|
||||
// Initialize the SSH allow/deny policy engine
|
||||
if p.sshPolicy, err = newSSHPolicyEngine(p.Options.GetSSHOptions()); err != nil {
|
||||
// Initialize the SSH allow/deny policy engine for user certificates
|
||||
if p.sshUserPolicy, err = 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 {
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -275,7 +280,7 @@ func (p *Nebula) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOpti
|
|||
// Require all the fields in the SSH certificate
|
||||
&sshCertDefaultValidator{},
|
||||
// Ensure that all principal names are allowed
|
||||
newSSHNamePolicyValidator(p.sshPolicy),
|
||||
newSSHNamePolicyValidator(p.sshHostPolicy, p.sshUserPolicy),
|
||||
), nil
|
||||
}
|
||||
|
||||
|
|
|
@ -13,7 +13,6 @@ import (
|
|||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/smallstep/certificates/errs"
|
||||
"github.com/smallstep/certificates/policy"
|
||||
"go.step.sm/crypto/jose"
|
||||
"go.step.sm/crypto/sshutil"
|
||||
"go.step.sm/crypto/x509util"
|
||||
|
@ -95,8 +94,9 @@ type OIDC struct {
|
|||
keyStore *keyStore
|
||||
claimer *Claimer
|
||||
getIdentityFunc GetIdentityFunc
|
||||
x509Policy policy.X509NamePolicyEngine
|
||||
sshPolicy policy.SSHNamePolicyEngine
|
||||
x509Policy x509PolicyEngine
|
||||
sshHostPolicy *hostPolicyEngine
|
||||
sshUserPolicy *userPolicyEngine
|
||||
}
|
||||
|
||||
func sanitizeEmail(email string) string {
|
||||
|
@ -216,8 +216,13 @@ func (o *OIDC) Init(config Config) (err error) {
|
|||
return err
|
||||
}
|
||||
|
||||
// Initialize the SSH allow/deny policy engine
|
||||
if o.sshPolicy, err = newSSHPolicyEngine(o.Options.GetSSHOptions()); err != nil {
|
||||
// Initialize the SSH allow/deny policy engine for user certificates
|
||||
if o.sshUserPolicy, err = 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 {
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -468,7 +473,7 @@ func (o *OIDC) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption
|
|||
// Require all the fields in the SSH certificate
|
||||
&sshCertDefaultValidator{},
|
||||
// Ensure that all principal names are allowed
|
||||
newSSHNamePolicyValidator(o.sshPolicy),
|
||||
newSSHNamePolicyValidator(o.sshHostPolicy, o.sshUserPolicy),
|
||||
), nil
|
||||
}
|
||||
|
||||
|
|
|
@ -1,11 +1,38 @@
|
|||
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) (policy.X509NamePolicyEngine, error) {
|
||||
func newX509PolicyEngine(x509Opts *X509Options) (x509PolicyEngine, error) {
|
||||
|
||||
if x509Opts == nil {
|
||||
return nil, nil
|
||||
|
@ -38,16 +65,66 @@ func newX509PolicyEngine(x509Opts *X509Options) (policy.X509NamePolicyEngine, er
|
|||
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) (policy.SSHNamePolicyEngine, error) {
|
||||
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{}
|
||||
|
||||
allowed := sshOpts.GetAllowedNameOptions()
|
||||
if allowed != nil && allowed.HasNames() {
|
||||
options = append(options,
|
||||
policy.WithPermittedDNSDomains(allowed.DNSDomains),
|
||||
|
@ -57,7 +134,6 @@ func newSSHPolicyEngine(sshOpts *SSHOptions) (policy.SSHNamePolicyEngine, error)
|
|||
)
|
||||
}
|
||||
|
||||
denied := sshOpts.GetDeniedNameOptions()
|
||||
if denied != nil && denied.HasNames() {
|
||||
options = append(options,
|
||||
policy.WithExcludedDNSDomains(denied.DNSDomains),
|
||||
|
@ -67,5 +143,14 @@ func newSSHPolicyEngine(sshOpts *SSHOptions) (policy.SSHNamePolicyEngine, error)
|
|||
)
|
||||
}
|
||||
|
||||
// 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...)
|
||||
}
|
||||
|
|
|
@ -16,7 +16,6 @@ import (
|
|||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/smallstep/certificates/errs"
|
||||
"github.com/smallstep/certificates/policy"
|
||||
"go.step.sm/crypto/keyutil"
|
||||
"go.step.sm/crypto/x509util"
|
||||
)
|
||||
|
@ -408,11 +407,11 @@ 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 policy.X509NamePolicyEngine
|
||||
policyEngine x509PolicyEngine
|
||||
}
|
||||
|
||||
// newX509NamePolicyValidator return a new SANs allow/deny validator.
|
||||
func newX509NamePolicyValidator(engine policy.X509NamePolicyEngine) *x509NamePolicyValidator {
|
||||
func newX509NamePolicyValidator(engine x509PolicyEngine) *x509NamePolicyValidator {
|
||||
return &x509NamePolicyValidator{
|
||||
policyEngine: engine,
|
||||
}
|
||||
|
@ -424,7 +423,6 @@ func (v *x509NamePolicyValidator) Valid(cert *x509.Certificate, _ SignOptions) e
|
|||
if v.policyEngine == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
_, err := v.policyEngine.AreCertificateNamesAllowed(cert)
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -4,13 +4,13 @@ import (
|
|||
"crypto/rsa"
|
||||
"encoding/binary"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/smallstep/certificates/errs"
|
||||
"github.com/smallstep/certificates/policy"
|
||||
"go.step.sm/crypto/keyutil"
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
@ -448,24 +448,55 @@ func (v sshDefaultPublicKeyValidator) Valid(cert *ssh.Certificate, o SignSSHOpti
|
|||
// sshNamePolicyValidator validates that the certificate (to be signed)
|
||||
// contains only allowed principals.
|
||||
type sshNamePolicyValidator struct {
|
||||
policyEngine policy.SSHNamePolicyEngine
|
||||
hostPolicyEngine *hostPolicyEngine
|
||||
userPolicyEngine *userPolicyEngine
|
||||
}
|
||||
|
||||
// newSSHNamePolicyValidator return a new SSH allow/deny validator.
|
||||
func newSSHNamePolicyValidator(engine policy.SSHNamePolicyEngine) *sshNamePolicyValidator {
|
||||
func newSSHNamePolicyValidator(host *hostPolicyEngine, user *userPolicyEngine) *sshNamePolicyValidator {
|
||||
return &sshNamePolicyValidator{
|
||||
policyEngine: engine,
|
||||
hostPolicyEngine: host,
|
||||
userPolicyEngine: user,
|
||||
}
|
||||
}
|
||||
|
||||
// Valid validates validates that the certificate (to be signed)
|
||||
// contains only allowed principals.
|
||||
func (v *sshNamePolicyValidator) Valid(cert *ssh.Certificate, _ SignSSHOptions) error {
|
||||
if v.policyEngine == nil {
|
||||
if v.hostPolicyEngine == nil && v.userPolicyEngine == nil {
|
||||
// no policy configured at all; allow anything
|
||||
return nil
|
||||
}
|
||||
_, err := v.policyEngine.ArePrincipalsAllowed(cert)
|
||||
|
||||
// Check the policy type to execute based on type of the certificate.
|
||||
// We don't allow user certs if only a host policy engine is configured and
|
||||
// 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:
|
||||
// when no host policy engine is configured, but a user policy engine is
|
||||
// configured, we don't allow the host certificate.
|
||||
if v.hostPolicyEngine == nil && v.userPolicyEngine != nil {
|
||||
return errors.New("SSH host certificate not authorized") // TODO: include principals in message?
|
||||
}
|
||||
_, err := v.hostPolicyEngine.ArePrincipalsAllowed(cert)
|
||||
return err
|
||||
case userPolicyEngineType:
|
||||
// when no user policy engine is configured, but a host policy engine is
|
||||
// configured, we don't allow the user certificate.
|
||||
if v.userPolicyEngine == nil && v.hostPolicyEngine != nil {
|
||||
return errors.New("SSH user certificate not authorized") // TODO: include principals in message?
|
||||
}
|
||||
_, err := v.userPolicyEngine.ArePrincipalsAllowed(cert)
|
||||
return err
|
||||
default:
|
||||
return fmt.Errorf("unexpected policy engine type %q", policyType) // satisfy return; shouldn't happen
|
||||
}
|
||||
}
|
||||
|
||||
// sshCertTypeUInt32
|
||||
|
|
|
@ -34,6 +34,15 @@ type SSHOptions struct {
|
|||
// templates.
|
||||
TemplateData json.RawMessage `json:"templateData,omitempty"`
|
||||
|
||||
// User contains SSH user certificate options.
|
||||
User *SSHUserCertificateOptions `json:"user,omitempty"`
|
||||
|
||||
// Host contains SSH host certificate options.
|
||||
Host *SSHHostCertificateOptions `json:"host,omitempty"`
|
||||
}
|
||||
|
||||
// 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"`
|
||||
|
||||
|
@ -41,6 +50,11 @@ type SSHOptions struct {
|
|||
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"`
|
||||
|
@ -56,7 +70,7 @@ func (o *SSHOptions) HasTemplate() bool {
|
|||
|
||||
// GetAllowedNameOptions returns the AllowedSSHNameOptions, which models the
|
||||
// names that a provisioner is authorized to sign SSH certificates for.
|
||||
func (o *SSHOptions) GetAllowedNameOptions() *SSHNameOptions {
|
||||
func (o *SSHUserCertificateOptions) GetAllowedNameOptions() *SSHNameOptions {
|
||||
if o == nil {
|
||||
return nil
|
||||
}
|
||||
|
@ -65,7 +79,7 @@ func (o *SSHOptions) GetAllowedNameOptions() *SSHNameOptions {
|
|||
|
||||
// GetDeniedNameOptions returns the DeniedSSHNameOptions, which models the
|
||||
// names that a provisioner is NOT authorized to sign SSH certificates for.
|
||||
func (o *SSHOptions) GetDeniedNameOptions() *SSHNameOptions {
|
||||
func (o *SSHUserCertificateOptions) GetDeniedNameOptions() *SSHNameOptions {
|
||||
if o == nil {
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -9,7 +9,6 @@ import (
|
|||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/smallstep/certificates/errs"
|
||||
"github.com/smallstep/certificates/policy"
|
||||
"go.step.sm/crypto/jose"
|
||||
"go.step.sm/crypto/sshutil"
|
||||
"go.step.sm/crypto/x509util"
|
||||
|
@ -36,8 +35,9 @@ type X5C struct {
|
|||
claimer *Claimer
|
||||
audiences Audiences
|
||||
rootPool *x509.CertPool
|
||||
x509Policy policy.X509NamePolicyEngine
|
||||
sshPolicy policy.SSHNamePolicyEngine
|
||||
x509Policy x509PolicyEngine
|
||||
sshHostPolicy *hostPolicyEngine
|
||||
sshUserPolicy *userPolicyEngine
|
||||
}
|
||||
|
||||
// GetID returns the provisioner unique identifier. The name and credential id
|
||||
|
@ -133,8 +133,13 @@ func (p *X5C) Init(config Config) error {
|
|||
return err
|
||||
}
|
||||
|
||||
// Initialize the SSH allow/deny policy engine
|
||||
if p.sshPolicy, err = newSSHPolicyEngine(p.Options.GetSSHOptions()); err != nil {
|
||||
// Initialize the SSH allow/deny policy engine for user certificates
|
||||
if p.sshUserPolicy, err = 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 {
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -326,6 +331,6 @@ func (p *X5C) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption,
|
|||
// Require all the fields in the SSH certificate
|
||||
&sshCertDefaultValidator{},
|
||||
// Ensure that all principal names are allowed
|
||||
newSSHNamePolicyValidator(p.sshPolicy),
|
||||
newSSHNamePolicyValidator(p.sshHostPolicy, p.sshUserPolicy),
|
||||
), nil
|
||||
}
|
||||
|
|
|
@ -780,7 +780,8 @@ func TestX5C_AuthorizeSSHSign(t *testing.T) {
|
|||
case *sshCertValidityValidator:
|
||||
assert.Equals(t, v.Claimer, tc.p.claimer)
|
||||
case *sshNamePolicyValidator:
|
||||
assert.Equals(t, nil, v.policyEngine)
|
||||
assert.Equals(t, nil, v.userPolicyEngine)
|
||||
assert.Equals(t, nil, v.hostPolicyEngine)
|
||||
case *sshDefaultPublicKeyValidator, *sshCertDefaultValidator, sshCertificateOptionsFunc:
|
||||
default:
|
||||
assert.FatalError(t, errors.Errorf("unexpected sign option of type %T", v))
|
||||
|
|
|
@ -216,11 +216,11 @@ func (e *NamePolicyEngine) IsIPAllowed(ip net.IP) (bool, error) {
|
|||
|
||||
// ArePrincipalsAllowed verifies that all principals in an SSH certificate are allowed.
|
||||
func (e *NamePolicyEngine) ArePrincipalsAllowed(cert *ssh.Certificate) (bool, error) {
|
||||
dnsNames, ips, emails, usernames, err := splitSSHPrincipals(cert)
|
||||
dnsNames, ips, emails, principals, err := splitSSHPrincipals(cert)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if err := e.validateNames(dnsNames, ips, emails, []*url.URL{}, usernames); err != nil {
|
||||
if err := e.validateNames(dnsNames, ips, emails, []*url.URL{}, principals); err != nil {
|
||||
return false, err
|
||||
}
|
||||
return true, nil
|
||||
|
@ -243,32 +243,26 @@ func appendSubjectCommonName(subject pkix.Name, dnsNames *[]string, ips *[]net.I
|
|||
}
|
||||
|
||||
// splitPrincipals splits SSH certificate principals into DNS names, emails and usernames.
|
||||
func splitSSHPrincipals(cert *ssh.Certificate) (dnsNames []string, ips []net.IP, emails, usernames []string, err error) {
|
||||
func splitSSHPrincipals(cert *ssh.Certificate) (dnsNames []string, ips []net.IP, emails, principals []string, err error) {
|
||||
dnsNames = []string{}
|
||||
ips = []net.IP{}
|
||||
emails = []string{}
|
||||
usernames = []string{}
|
||||
principals = []string{}
|
||||
var uris []*url.URL
|
||||
switch cert.CertType {
|
||||
case ssh.HostCert:
|
||||
dnsNames, ips, emails, uris = x509util.SplitSANs(cert.ValidPrincipals)
|
||||
switch {
|
||||
case len(emails) > 0:
|
||||
err = fmt.Errorf("Email(-like) principals %v not expected in SSH Host certificate ", emails)
|
||||
case len(uris) > 0:
|
||||
err = fmt.Errorf("URL principals %v not expected in SSH Host certificate ", uris)
|
||||
if len(uris) > 0 {
|
||||
err = fmt.Errorf("URL principals %v not expected in SSH host certificate ", uris)
|
||||
}
|
||||
case ssh.UserCert:
|
||||
// re-using SplitSANs results in anything that can't be parsed as an IP, URI or email
|
||||
// to be considered a username. This allows usernames like h.slatman to be present
|
||||
// to be considered a username principal. This allows usernames like h.slatman to be present
|
||||
// in the SSH certificate. We're exluding IPs and URIs, because they can be confusing
|
||||
// when used in a SSH user certificate.
|
||||
usernames, ips, emails, uris = x509util.SplitSANs(cert.ValidPrincipals)
|
||||
switch {
|
||||
case len(ips) > 0:
|
||||
err = fmt.Errorf("IP principals %v not expected in SSH User certificate ", ips)
|
||||
case len(uris) > 0:
|
||||
err = fmt.Errorf("URL principals %v not expected in SSH User certificate ", uris)
|
||||
principals, ips, emails, uris = x509util.SplitSANs(cert.ValidPrincipals)
|
||||
if len(uris) > 0 {
|
||||
err = fmt.Errorf("URL principals %v not expected in SSH user certificate ", uris)
|
||||
}
|
||||
default:
|
||||
err = fmt.Errorf("unexpected SSH certificate type %d", cert.CertType)
|
||||
|
@ -280,7 +274,7 @@ func splitSSHPrincipals(cert *ssh.Certificate) (dnsNames []string, ips []net.IP,
|
|||
// validateNames verifies that all names are allowed.
|
||||
// Its logic follows that of (a large part of) the (c *Certificate) isValid() function
|
||||
// in https://cs.opensource.google/go/go/+/refs/tags/go1.17.5:src/crypto/x509/verify.go
|
||||
func (e *NamePolicyEngine) validateNames(dnsNames []string, ips []net.IP, emailAddresses []string, uris []*url.URL, usernames []string) error {
|
||||
func (e *NamePolicyEngine) validateNames(dnsNames []string, ips []net.IP, emailAddresses []string, uris []*url.URL, principals []string) error {
|
||||
|
||||
// nothing to compare against; return early
|
||||
if e.totalNumberOfConstraints == 0 {
|
||||
|
@ -400,15 +394,15 @@ func (e *NamePolicyEngine) validateNames(dnsNames []string, ips []net.IP, emailA
|
|||
}
|
||||
}
|
||||
|
||||
for _, username := range usernames {
|
||||
for _, principal := range principals {
|
||||
if e.numberOfPrincipalConstraints == 0 && e.totalNumberOfPermittedConstraints > 0 {
|
||||
return NamePolicyError{
|
||||
Reason: NotAuthorizedForThisName,
|
||||
Detail: fmt.Sprintf("username principal %q is not explicitly permitted by any constraint", username),
|
||||
Detail: fmt.Sprintf("username principal %q is not explicitly permitted by any constraint", principal),
|
||||
}
|
||||
}
|
||||
// TODO: some validation? I.e. allowed characters?
|
||||
if err := checkNameConstraints("username", username, username,
|
||||
if err := checkNameConstraints("principal", principal, principal,
|
||||
func(parsedName, constraint interface{}) (bool, error) {
|
||||
return matchUsernameConstraint(parsedName.(string), constraint.(string))
|
||||
}, e.permittedPrincipals, e.excludedPrincipals); err != nil {
|
||||
|
|
|
@ -2749,18 +2749,6 @@ func Test_splitSSHPrincipals(t *testing.T) {
|
|||
wantErr: true,
|
||||
}
|
||||
},
|
||||
"fail/user-ip": func(t *testing.T) test {
|
||||
r := emptyResult()
|
||||
r.wantIps = []net.IP{net.ParseIP("127.0.0.1")} // this will still be in the result
|
||||
return test{
|
||||
cert: &ssh.Certificate{
|
||||
CertType: ssh.UserCert,
|
||||
ValidPrincipals: []string{"127.0.0.1"},
|
||||
},
|
||||
r: r,
|
||||
wantErr: true,
|
||||
}
|
||||
},
|
||||
"fail/user-uri": func(t *testing.T) test {
|
||||
r := emptyResult()
|
||||
return test{
|
||||
|
@ -2772,18 +2760,6 @@ func Test_splitSSHPrincipals(t *testing.T) {
|
|||
wantErr: true,
|
||||
}
|
||||
},
|
||||
"fail/host-email": func(t *testing.T) test {
|
||||
r := emptyResult()
|
||||
r.wantEmails = []string{"ops@work"} // this will still be in the result
|
||||
return test{
|
||||
cert: &ssh.Certificate{
|
||||
CertType: ssh.HostCert,
|
||||
ValidPrincipals: []string{"ops@work"},
|
||||
},
|
||||
r: r,
|
||||
wantErr: true,
|
||||
}
|
||||
},
|
||||
"fail/host-uri": func(t *testing.T) test {
|
||||
r := emptyResult()
|
||||
return test{
|
||||
|
@ -2817,6 +2793,18 @@ func Test_splitSSHPrincipals(t *testing.T) {
|
|||
r: r,
|
||||
}
|
||||
},
|
||||
"ok/host-email": func(t *testing.T) test {
|
||||
r := emptyResult()
|
||||
r.wantEmails = []string{"ops@work"}
|
||||
return test{
|
||||
cert: &ssh.Certificate{
|
||||
CertType: ssh.HostCert,
|
||||
ValidPrincipals: []string{"ops@work"},
|
||||
},
|
||||
r: r,
|
||||
wantErr: false,
|
||||
}
|
||||
},
|
||||
"ok/user-localhost": func(t *testing.T) test {
|
||||
r := emptyResult()
|
||||
r.wantUsernames = []string{"localhost"} // when type is User cert, this is considered a username; not a DNS
|
||||
|
@ -2839,6 +2827,18 @@ func Test_splitSSHPrincipals(t *testing.T) {
|
|||
r: r,
|
||||
}
|
||||
},
|
||||
"ok/user-ip": func(t *testing.T) test {
|
||||
r := emptyResult()
|
||||
r.wantIps = []net.IP{net.ParseIP("127.0.0.1")}
|
||||
return test{
|
||||
cert: &ssh.Certificate{
|
||||
CertType: ssh.UserCert,
|
||||
ValidPrincipals: []string{"127.0.0.1"},
|
||||
},
|
||||
r: r,
|
||||
wantErr: false,
|
||||
}
|
||||
},
|
||||
"ok/user-maillike": func(t *testing.T) test {
|
||||
r := emptyResult()
|
||||
r.wantEmails = []string{"ops@work"}
|
||||
|
|
Loading…
Reference in a new issue