Add support for SSH certificates to OIDC.
Update the interface for all the provisioners.
This commit is contained in:
parent
a44b0a1d52
commit
f01286bb48
9 changed files with 147 additions and 13 deletions
|
@ -1,6 +1,7 @@
|
|||
package provisioner
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
|
@ -266,13 +267,18 @@ func (p *AWS) Init(config Config) (err error) {
|
|||
|
||||
// AuthorizeSign validates the given token and returns the sign options that
|
||||
// will be used on certificate creation.
|
||||
func (p *AWS) AuthorizeSign(token string) ([]SignOption, error) {
|
||||
func (p *AWS) AuthorizeSign(ctx context.Context, token string) ([]SignOption, error) {
|
||||
payload, err := p.authorizeToken(token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
doc := payload.document
|
||||
|
||||
// Check for the sign ssh method, default to sign X.509
|
||||
if m := MethodFromContext(ctx); m == SignSSHMethod {
|
||||
return p.authorizeSSHSign(payload)
|
||||
}
|
||||
|
||||
doc := payload.document
|
||||
// Enforce known CN and default DNS and IP if configured.
|
||||
// By default we'll accept the CN and SANs in the CSR.
|
||||
// There's no way to trust them other than TOFU.
|
||||
|
@ -432,3 +438,8 @@ func (p *AWS) authorizeToken(token string) (*awsPayload, error) {
|
|||
payload.document = doc
|
||||
return &payload, nil
|
||||
}
|
||||
|
||||
// authorizeSSHSign returns the list of SignOption for a SignSSH request.
|
||||
func (p *AWS) authorizeSSHSign(claims *awsPayload) ([]SignOption, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package provisioner
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"crypto/x509"
|
||||
"encoding/hex"
|
||||
|
@ -209,7 +210,7 @@ func (p *Azure) Init(config Config) (err error) {
|
|||
|
||||
// AuthorizeSign validates the given token and returns the sign options that
|
||||
// will be used on certificate creation.
|
||||
func (p *Azure) AuthorizeSign(token string) ([]SignOption, error) {
|
||||
func (p *Azure) AuthorizeSign(ctx context.Context, token string) ([]SignOption, error) {
|
||||
jwt, err := jose.ParseSigned(token)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "error parsing token")
|
||||
|
@ -264,6 +265,11 @@ func (p *Azure) AuthorizeSign(token string) ([]SignOption, error) {
|
|||
}
|
||||
}
|
||||
|
||||
// Check for the sign ssh method, default to sign X.509
|
||||
if m := MethodFromContext(ctx); m == SignSSHMethod {
|
||||
return p.authorizeSSHSign(claims)
|
||||
}
|
||||
|
||||
// Enforce known common name and default DNS if configured.
|
||||
// By default we'll accept the CN and SANs in the CSR.
|
||||
// There's no way to trust them other than TOFU.
|
||||
|
@ -295,6 +301,11 @@ func (p *Azure) AuthorizeRevoke(token string) error {
|
|||
return errors.New("revoke is not supported on a Azure provisioner")
|
||||
}
|
||||
|
||||
// authorizeSSHSign returns the list of SignOption for a SignSSH request.
|
||||
func (p *Azure) authorizeSSHSign(claims azurePayload) ([]SignOption, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// assertConfig initializes the config if it has not been initialized
|
||||
func (p *Azure) assertConfig() {
|
||||
if p.config == nil {
|
||||
|
|
|
@ -2,6 +2,7 @@ package provisioner
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"crypto/x509"
|
||||
"encoding/hex"
|
||||
|
@ -205,13 +206,18 @@ func (p *GCP) Init(config Config) error {
|
|||
|
||||
// AuthorizeSign validates the given token and returns the sign options that
|
||||
// will be used on certificate creation.
|
||||
func (p *GCP) AuthorizeSign(token string) ([]SignOption, error) {
|
||||
func (p *GCP) AuthorizeSign(ctx context.Context, token string) ([]SignOption, error) {
|
||||
claims, err := p.authorizeToken(token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ce := claims.Google.ComputeEngine
|
||||
|
||||
// Check for the sign ssh method, default to sign X.509
|
||||
if m := MethodFromContext(ctx); m == SignSSHMethod {
|
||||
return p.authorizeSSHSign(claims)
|
||||
}
|
||||
|
||||
ce := claims.Google.ComputeEngine
|
||||
// Enforce known common name and default DNS if configured.
|
||||
// By default we we'll accept the CN and SANs in the CSR.
|
||||
// There's no way to trust them other than TOFU.
|
||||
|
@ -344,3 +350,8 @@ func (p *GCP) authorizeToken(token string) (*gcpPayload, error) {
|
|||
|
||||
return &claims, nil
|
||||
}
|
||||
|
||||
// authorizeSSHSign returns the list of SignOption for a SignSSH request.
|
||||
func (p *GCP) authorizeSSHSign(claims *gcpPayload) ([]SignOption, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package provisioner
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/x509"
|
||||
"time"
|
||||
|
||||
|
@ -134,7 +135,7 @@ func (p *JWK) AuthorizeRevoke(token string) error {
|
|||
}
|
||||
|
||||
// AuthorizeSign validates the given token.
|
||||
func (p *JWK) AuthorizeSign(token string) ([]SignOption, error) {
|
||||
func (p *JWK) AuthorizeSign(ctx context.Context, token string) ([]SignOption, error) {
|
||||
claims, err := p.authorizeToken(token, p.audiences.Sign)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -171,6 +172,7 @@ func (p *JWK) AuthorizeRenewal(cert *x509.Certificate) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// authorizeSSHSign returns the list of SignOption for a SignSSH request.
|
||||
func (p *JWK) authorizeSSHSign(claims *jwtPayload) ([]SignOption, error) {
|
||||
t := now()
|
||||
opts := claims.Step.SSH
|
||||
|
|
34
authority/provisioner/method.go
Normal file
34
authority/provisioner/method.go
Normal file
|
@ -0,0 +1,34 @@
|
|||
package provisioner
|
||||
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
|
||||
// Method indicates the action to action that we will perform, it's used as part
|
||||
// of the context in the call to authorize. It defaults to Sing.
|
||||
type Method int
|
||||
|
||||
// The key to save the Method in the context.
|
||||
type methodKey struct{}
|
||||
|
||||
const (
|
||||
// SignMethod is the method used to sign X.509 certificates.
|
||||
SignMethod Method = iota
|
||||
// SignSSHMethod is the method used to sign SSH certificate.
|
||||
SignSSHMethod
|
||||
// RevokeMethod is the method used to revoke X.509 certificates.
|
||||
RevokeMethod
|
||||
)
|
||||
|
||||
// NewContextWithMethod creates a new context from ctx and attaches method to
|
||||
// it.
|
||||
func NewContextWithMethod(ctx context.Context, method Method) context.Context {
|
||||
return context.WithValue(ctx, methodKey{}, method)
|
||||
}
|
||||
|
||||
// MethodFromContext returns the Method saved in ctx. Returns Sign if the given
|
||||
// context has no Method associated with it.
|
||||
func MethodFromContext(ctx context.Context) Method {
|
||||
m, _ := ctx.Value(methodKey{}).(Method)
|
||||
return m
|
||||
}
|
|
@ -1,6 +1,9 @@
|
|||
package provisioner
|
||||
|
||||
import "crypto/x509"
|
||||
import (
|
||||
"context"
|
||||
"crypto/x509"
|
||||
)
|
||||
|
||||
// noop provisioners is a provisioner that accepts anything.
|
||||
type noop struct{}
|
||||
|
@ -28,7 +31,7 @@ func (p *noop) Init(config Config) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (p *noop) AuthorizeSign(token string) ([]SignOption, error) {
|
||||
func (p *noop) AuthorizeSign(ctx context.Context, token string) ([]SignOption, error) {
|
||||
return []SignOption{}, nil
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package provisioner
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/x509"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
@ -259,11 +260,17 @@ func (o *OIDC) AuthorizeRevoke(token string) error {
|
|||
}
|
||||
|
||||
// AuthorizeSign validates the given token.
|
||||
func (o *OIDC) AuthorizeSign(token string) ([]SignOption, error) {
|
||||
func (o *OIDC) AuthorizeSign(ctx context.Context, token string) ([]SignOption, error) {
|
||||
claims, err := o.authorizeToken(token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Check for the sign ssh method, default to sign X.509
|
||||
if m := MethodFromContext(ctx); m == SignSSHMethod {
|
||||
return o.authorizeSSHSign(claims)
|
||||
}
|
||||
|
||||
// Admins should be able to authorize any SAN
|
||||
if o.IsAdmin(claims.Email) {
|
||||
return []SignOption{
|
||||
|
@ -289,6 +296,37 @@ func (o *OIDC) AuthorizeRenewal(cert *x509.Certificate) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// authorizeSSHSign returns the list of SignOption for a SignSSH request.
|
||||
func (o *OIDC) authorizeSSHSign(claims *openIDPayload) ([]SignOption, error) {
|
||||
signOptions := []SignOption{
|
||||
// set the default extensions
|
||||
&sshDefaultExtensionModifier{},
|
||||
// set the key id to the token subject
|
||||
sshCertificateKeyIDModifier(claims.Email),
|
||||
}
|
||||
|
||||
// Non-admins are only able to sign user certificates
|
||||
if o.IsAdmin(claims.Email) {
|
||||
signOptions = append(signOptions, &sshCertificateOptionsValidator{})
|
||||
} else {
|
||||
name := principalFromEmail(claims.Email)
|
||||
if !sshUserRegex.MatchString(name) {
|
||||
return nil, errors.Errorf("invalid principal '%s' from email address '%s'", name, claims.Email)
|
||||
}
|
||||
signOptions = append(signOptions, &sshCertificateOptionsValidator{&SSHOptions{
|
||||
CertType: SSHUserCert,
|
||||
Principals: []string{name},
|
||||
}})
|
||||
}
|
||||
|
||||
return append(signOptions,
|
||||
// checks the validity bounds, and set the validity if has not been set
|
||||
&sshCertificateValidityModifier{o.claimer},
|
||||
// require all the fields in the SSH certificate
|
||||
&sshCertificateDefaultValidator{},
|
||||
), nil
|
||||
}
|
||||
|
||||
func getAndDecode(uri string, v interface{}) error {
|
||||
resp, err := http.Get(uri)
|
||||
if err != nil {
|
||||
|
@ -300,3 +338,21 @@ func getAndDecode(uri string, v interface{}) error {
|
|||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func principalFromEmail(email string) string {
|
||||
if i := strings.LastIndex(email, "@"); i >= 0 {
|
||||
email = email[:i]
|
||||
}
|
||||
return strings.Map(func(r rune) rune {
|
||||
switch {
|
||||
case r >= 'a' && r <= 'z':
|
||||
return r
|
||||
case r >= '0' && r <= '9':
|
||||
return r
|
||||
case r == '.': // drop dots
|
||||
return -1
|
||||
default:
|
||||
return '_'
|
||||
}
|
||||
}, strings.ToLower(email))
|
||||
}
|
||||
|
|
|
@ -1,14 +1,18 @@
|
|||
package provisioner
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/x509"
|
||||
"encoding/json"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
var sshUserRegex = regexp.MustCompile("^[a-z][-a-z0-9_]*$")
|
||||
|
||||
// Interface is the interface that all provisioner types must implement.
|
||||
type Interface interface {
|
||||
GetID() string
|
||||
|
@ -17,7 +21,7 @@ type Interface interface {
|
|||
GetType() Type
|
||||
GetEncryptedKey() (kid string, key string, ok bool)
|
||||
Init(config Config) error
|
||||
AuthorizeSign(token string) ([]SignOption, error)
|
||||
AuthorizeSign(ctx context.Context, token string) ([]SignOption, error)
|
||||
AuthorizeRenewal(cert *x509.Certificate) error
|
||||
AuthorizeRevoke(token string) error
|
||||
}
|
||||
|
|
|
@ -205,11 +205,13 @@ func (m *sshCertificateValidityModifier) Modify(cert *ssh.Certificate) error {
|
|||
// sshCertificateOptionsValidator validates the user SSHOptions with the ones
|
||||
// usually present in the token.
|
||||
type sshCertificateOptionsValidator struct {
|
||||
*SSHOptions
|
||||
Want *SSHOptions
|
||||
}
|
||||
|
||||
func (want *sshCertificateOptionsValidator) Valid(got SSHOptions) error {
|
||||
return want.match(got)
|
||||
// Valid implements SSHCertificateOptionsValidator and returns nil if both
|
||||
// SSHOptions match.
|
||||
func (v *sshCertificateOptionsValidator) Valid(got SSHOptions) error {
|
||||
return v.Want.match(got)
|
||||
}
|
||||
|
||||
// sshCertificateDefaultValidator implements a simple validator for all the
|
||||
|
|
Loading…
Reference in a new issue