Customize principal validation using an environment variable

By default, the OIDC user principal must validate the regular expression
"^[a-z][-a-z0-9_]*$", but with this commit, a custom regular expression can be
defined using the environment variable STEP_SSH_USER_REGEXP.

Fixes #1436
This commit is contained in:
Mariano Cano 2023-07-18 15:18:24 -07:00
parent cbc46d11e5
commit d9c4b0cd1c
No known key found for this signature in database
2 changed files with 36 additions and 9 deletions

View file

@ -4,6 +4,7 @@ import (
"context" "context"
"crypto/x509" "crypto/x509"
"net/http" "net/http"
"os"
"regexp" "regexp"
"strings" "strings"
"time" "time"
@ -108,9 +109,13 @@ type AuthorizeRenewFunc func(ctx context.Context, p *Controller, cert *x509.Cert
// given SSH certificate is enabled. // given SSH certificate is enabled.
type AuthorizeSSHRenewFunc func(ctx context.Context, p *Controller, cert *ssh.Certificate) error type AuthorizeSSHRenewFunc func(ctx context.Context, p *Controller, cert *ssh.Certificate) error
// DefaultIdentityFunc return a default identity depending on the provisioner // DefaultIdentityFunc returns a default identity depending on the provisioner
// type. For OIDC email is always present and the usernames might // type. For OIDC email is always present and the usernames might contain empty
// contain empty strings. // strings.
//
// By default, the user principal from the OIDC email must validate the regular
// expression "^[a-z][-a-z0-9_]*$", but a custom regular expression can be
// defined using the environment variable STEP_SSH_USER_REGEXP.
func DefaultIdentityFunc(_ context.Context, p Interface, email string) (*Identity, error) { func DefaultIdentityFunc(_ context.Context, p Interface, email string) (*Identity, error) {
switch k := p.(type) { switch k := p.(type) {
case *OIDC: case *OIDC:
@ -120,7 +125,7 @@ func DefaultIdentityFunc(_ context.Context, p Interface, email string) (*Identit
// 3. Raw local (if different). // 3. Raw local (if different).
// 4. Email address. // 4. Email address.
name := SanitizeSSHUserPrincipal(email) name := SanitizeSSHUserPrincipal(email)
if !sshUserRegex.MatchString(name) { if !sshUserRegexp.MatchString(name) {
return nil, errors.Errorf("invalid principal '%s' from email '%s'", name, email) return nil, errors.Errorf("invalid principal '%s' from email '%s'", name, email)
} }
usernames := []string{name} usernames := []string{name}
@ -178,7 +183,16 @@ func DefaultAuthorizeSSHRenew(_ context.Context, p *Controller, cert *ssh.Certif
return nil return nil
} }
var sshUserRegex = regexp.MustCompile("^[a-z][-a-z0-9_]*$") // sshUserRegexp is the regular expression used to validate the SSH user
// principals in DefaultIdentityFunc.
var sshUserRegexp *regexp.Regexp
func init() {
if v := os.Getenv("STEP_SSH_USER_REGEXP"); v != "" {
sshUserRegexp = regexp.MustCompile(v)
}
sshUserRegexp = regexp.MustCompile("^[a-z][-a-z0-9_]*$")
}
// SanitizeStringSlices removes duplicated an empty strings. // SanitizeStringSlices removes duplicated an empty strings.
func SanitizeStringSlices(original []string) []string { func SanitizeStringSlices(original []string) []string {

View file

@ -5,6 +5,7 @@ import (
"crypto/x509" "crypto/x509"
"fmt" "fmt"
"reflect" "reflect"
"regexp"
"testing" "testing"
"time" "time"
@ -144,9 +145,15 @@ func TestNewController(t *testing.T) {
func TestController_GetIdentity(t *testing.T) { func TestController_GetIdentity(t *testing.T) {
ctx := context.Background() ctx := context.Background()
defaultUserRegexp := sshUserRegexp
t.Cleanup(func() {
sshUserRegexp = defaultUserRegexp
})
type fields struct { type fields struct {
Interface Interface Interface Interface
IdentityFunc GetIdentityFunc IdentityFunc GetIdentityFunc
SSHUserRegex *regexp.Regexp
} }
type args struct { type args struct {
ctx context.Context ctx context.Context
@ -159,21 +166,27 @@ func TestController_GetIdentity(t *testing.T) {
want *Identity want *Identity
wantErr bool wantErr bool
}{ }{
{"ok", fields{&OIDC{}, nil}, args{ctx, "jane@doe.org"}, &Identity{ {"ok", fields{&OIDC{}, nil, defaultUserRegexp}, args{ctx, "jane@doe.org"}, &Identity{
Usernames: []string{"jane", "jane@doe.org"}, Usernames: []string{"jane", "jane@doe.org"},
}, false}, }, false},
{"ok custom", fields{&OIDC{}, func(ctx context.Context, p Interface, email string) (*Identity, error) { {"ok custom", fields{&OIDC{}, func(ctx context.Context, p Interface, email string) (*Identity, error) {
return &Identity{Usernames: []string{"jane"}}, nil return &Identity{Usernames: []string{"jane"}}, nil
}}, args{ctx, "jane@doe.org"}, &Identity{ }, defaultUserRegexp}, args{ctx, "jane@doe.org"}, &Identity{
Usernames: []string{"jane"}, Usernames: []string{"jane"},
}, false}, }, false},
{"fail provisioner", fields{&JWK{}, nil}, args{ctx, "jane@doe.org"}, nil, true}, {"ok custom regex", fields{&OIDC{}, nil, regexp.MustCompile("^[a-z0-9]*$")}, args{ctx, "1000@doe.org"}, &Identity{
Usernames: []string{"1000", "1000@doe.org"},
}, false},
{"fail provisioner", fields{&JWK{}, nil, defaultUserRegexp}, args{ctx, "jane@doe.org"}, nil, true},
{"fail custom", fields{&OIDC{}, func(ctx context.Context, p Interface, email string) (*Identity, error) { {"fail custom", fields{&OIDC{}, func(ctx context.Context, p Interface, email string) (*Identity, error) {
return nil, fmt.Errorf("an error") return nil, fmt.Errorf("an error")
}}, args{ctx, "jane@doe.org"}, nil, true}, }, defaultUserRegexp}, args{ctx, "jane@doe.org"}, nil, true},
{"fail regex", fields{&OIDC{}, nil, defaultUserRegexp}, args{ctx, "1000@doe.org"}, nil, true},
{"fail custom regex", fields{&OIDC{}, nil, regexp.MustCompile("^[a-z]*$")}, args{ctx, "jane1000@doe.org"}, nil, true},
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
sshUserRegexp = tt.fields.SSHUserRegex
c := &Controller{ c := &Controller{
Interface: tt.fields.Interface, Interface: tt.fields.Interface,
IdentityFunc: tt.fields.IdentityFunc, IdentityFunc: tt.fields.IdentityFunc,