Merge pull request #829 from vijayjt/new-azure-token-authz-options

Add subscription and object ID validation options to Azure provisioner
This commit is contained in:
Mariano Cano 2022-02-28 14:31:28 -08:00 committed by GitHub
commit 6f46cdb432
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 97 additions and 14 deletions

View file

@ -89,6 +89,8 @@ type Azure struct {
Name string `json:"name"` Name string `json:"name"`
TenantID string `json:"tenantID"` TenantID string `json:"tenantID"`
ResourceGroups []string `json:"resourceGroups"` ResourceGroups []string `json:"resourceGroups"`
SubscriptionIDs []string `json:"subscriptionIDs"`
ObjectIDs []string `json:"ObjectIDs"`
Audience string `json:"audience,omitempty"` Audience string `json:"audience,omitempty"`
DisableCustomSANs bool `json:"disableCustomSANs"` DisableCustomSANs bool `json:"disableCustomSANs"`
DisableTrustOnFirstUse bool `json:"disableTrustOnFirstUse"` DisableTrustOnFirstUse bool `json:"disableTrustOnFirstUse"`
@ -224,14 +226,14 @@ func (p *Azure) Init(config Config) (err error) {
return nil return nil
} }
// authorizeToken returns the claims, name, group, error. // authorizeToken returns the claims, name, group, subscription, identityObjectID, error.
func (p *Azure) authorizeToken(token string) (*azurePayload, string, string, error) { func (p *Azure) authorizeToken(token string) (*azurePayload, string, string, string, string, error) {
jwt, err := jose.ParseSigned(token) jwt, err := jose.ParseSigned(token)
if err != nil { if err != nil {
return nil, "", "", errs.Wrap(http.StatusUnauthorized, err, "azure.authorizeToken; error parsing azure token") return nil, "", "", "", "", errs.Wrap(http.StatusUnauthorized, err, "azure.authorizeToken; error parsing azure token")
} }
if len(jwt.Headers) == 0 { if len(jwt.Headers) == 0 {
return nil, "", "", errs.Unauthorized("azure.authorizeToken; azure token missing header") return nil, "", "", "", "", errs.Unauthorized("azure.authorizeToken; azure token missing header")
} }
var found bool var found bool
@ -244,7 +246,7 @@ func (p *Azure) authorizeToken(token string) (*azurePayload, string, string, err
} }
} }
if !found { if !found {
return nil, "", "", errs.Unauthorized("azure.authorizeToken; cannot validate azure token") return nil, "", "", "", "", errs.Unauthorized("azure.authorizeToken; cannot validate azure token")
} }
if err := claims.ValidateWithLeeway(jose.Expected{ if err := claims.ValidateWithLeeway(jose.Expected{
@ -252,26 +254,27 @@ func (p *Azure) authorizeToken(token string) (*azurePayload, string, string, err
Issuer: p.oidcConfig.Issuer, Issuer: p.oidcConfig.Issuer,
Time: time.Now(), Time: time.Now(),
}, 1*time.Minute); err != nil { }, 1*time.Minute); err != nil {
return nil, "", "", errs.Wrap(http.StatusUnauthorized, err, "azure.authorizeToken; failed to validate azure token payload") return nil, "", "", "", "", errs.Wrap(http.StatusUnauthorized, err, "azure.authorizeToken; failed to validate azure token payload")
} }
// Validate TenantID // Validate TenantID
if claims.TenantID != p.TenantID { if claims.TenantID != p.TenantID {
return nil, "", "", errs.Unauthorized("azure.authorizeToken; azure token validation failed - invalid tenant id claim (tid)") return nil, "", "", "", "", errs.Unauthorized("azure.authorizeToken; azure token validation failed - invalid tenant id claim (tid)")
} }
re := azureXMSMirIDRegExp.FindStringSubmatch(claims.XMSMirID) re := azureXMSMirIDRegExp.FindStringSubmatch(claims.XMSMirID)
if len(re) != 4 { if len(re) != 4 {
return nil, "", "", errs.Unauthorized("azure.authorizeToken; error parsing xms_mirid claim - %s", claims.XMSMirID) return nil, "", "", "", "", errs.Unauthorized("azure.authorizeToken; error parsing xms_mirid claim - %s", claims.XMSMirID)
} }
group, name := re[2], re[3] identityObjectID := claims.ObjectID
return &claims, name, group, nil subscription, group, name := re[1], re[2], re[3]
return &claims, name, group, subscription, identityObjectID, nil
} }
// AuthorizeSign validates the given token and returns the sign options that // AuthorizeSign validates the given token and returns the sign options that
// will be used on certificate creation. // will be used on certificate creation.
func (p *Azure) AuthorizeSign(ctx context.Context, token string) ([]SignOption, error) { func (p *Azure) AuthorizeSign(ctx context.Context, token string) ([]SignOption, error) {
_, name, group, err := p.authorizeToken(token) _, name, group, subscription, identityObjectID, err := p.authorizeToken(token)
if err != nil { if err != nil {
return nil, errs.Wrap(http.StatusInternalServerError, err, "azure.AuthorizeSign") return nil, errs.Wrap(http.StatusInternalServerError, err, "azure.AuthorizeSign")
} }
@ -290,6 +293,34 @@ func (p *Azure) AuthorizeSign(ctx context.Context, token string) ([]SignOption,
} }
} }
// Filter by subscription id
if len(p.SubscriptionIDs) > 0 {
var found bool
for _, s := range p.SubscriptionIDs {
if s == subscription {
found = true
break
}
}
if !found {
return nil, errs.Unauthorized("azure.AuthorizeSign; azure token validation failed - invalid subscription id")
}
}
// Filter by Azure AD identity object id
if len(p.ObjectIDs) > 0 {
var found bool
for _, i := range p.ObjectIDs {
if i == identityObjectID {
found = true
break
}
}
if !found {
return nil, errs.Unauthorized("azure.AuthorizeSign; azure token validation failed - invalid identity object id")
}
}
// Template options // Template options
data := x509util.NewTemplateData() data := x509util.NewTemplateData()
data.SetCommonName(name) data.SetCommonName(name)
@ -348,7 +379,7 @@ func (p *Azure) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOptio
return nil, errs.Unauthorized("azure.AuthorizeSSHSign; sshCA is disabled for provisioner '%s'", p.GetName()) return nil, errs.Unauthorized("azure.AuthorizeSSHSign; sshCA is disabled for provisioner '%s'", p.GetName())
} }
_, name, _, err := p.authorizeToken(token) _, name, _, _, _, err := p.authorizeToken(token)
if err != nil { if err != nil {
return nil, errs.Wrap(http.StatusInternalServerError, err, "azure.AuthorizeSSHSign") return nil, errs.Wrap(http.StatusInternalServerError, err, "azure.AuthorizeSSHSign")
} }

View file

@ -333,7 +333,7 @@ func TestAzure_authorizeToken(t *testing.T) {
for name, tt := range tests { for name, tt := range tests {
t.Run(name, func(t *testing.T) { t.Run(name, func(t *testing.T) {
tc := tt(t) tc := tt(t)
if claims, name, group, err := tc.p.authorizeToken(tc.token); err != nil { if claims, name, group, subscriptionID, objectID, err := tc.p.authorizeToken(tc.token); err != nil {
if assert.NotNil(t, tc.err) { if assert.NotNil(t, tc.err) {
sc, ok := err.(errs.StatusCoder) sc, ok := err.(errs.StatusCoder)
assert.Fatal(t, ok, "error does not implement StatusCoder interface") assert.Fatal(t, ok, "error does not implement StatusCoder interface")
@ -348,6 +348,8 @@ func TestAzure_authorizeToken(t *testing.T) {
assert.Equals(t, name, "virtualMachine") assert.Equals(t, name, "virtualMachine")
assert.Equals(t, group, "resourceGroup") assert.Equals(t, group, "resourceGroup")
assert.Equals(t, subscriptionID, "subscriptionID")
assert.Equals(t, objectID, "the-oid")
} }
} }
}) })
@ -382,6 +384,38 @@ func TestAzure_AuthorizeSign(t *testing.T) {
p4.oidcConfig = p1.oidcConfig p4.oidcConfig = p1.oidcConfig
p4.keyStore = p1.keyStore p4.keyStore = p1.keyStore
p5, err := generateAzure()
assert.FatalError(t, err)
p5.TenantID = p1.TenantID
p5.SubscriptionIDs = []string{"subscriptionID"}
p5.config = p1.config
p5.oidcConfig = p1.oidcConfig
p5.keyStore = p1.keyStore
p6, err := generateAzure()
assert.FatalError(t, err)
p6.TenantID = p1.TenantID
p6.SubscriptionIDs = []string{"foobarzar"}
p6.config = p1.config
p6.oidcConfig = p1.oidcConfig
p6.keyStore = p1.keyStore
p7, err := generateAzure()
assert.FatalError(t, err)
p7.TenantID = p1.TenantID
p7.ObjectIDs = []string{"the-oid"}
p7.config = p1.config
p7.oidcConfig = p1.oidcConfig
p7.keyStore = p1.keyStore
p8, err := generateAzure()
assert.FatalError(t, err)
p8.TenantID = p1.TenantID
p8.ObjectIDs = []string{"foobarzar"}
p8.config = p1.config
p8.oidcConfig = p1.oidcConfig
p8.keyStore = p1.keyStore
badKey, err := generateJSONWebKey() badKey, err := generateJSONWebKey()
assert.FatalError(t, err) assert.FatalError(t, err)
@ -393,6 +427,14 @@ func TestAzure_AuthorizeSign(t *testing.T) {
assert.FatalError(t, err) assert.FatalError(t, err)
t4, err := p4.GetIdentityToken("subject", "caURL") t4, err := p4.GetIdentityToken("subject", "caURL")
assert.FatalError(t, err) assert.FatalError(t, err)
t5, err := p5.GetIdentityToken("subject", "caURL")
assert.FatalError(t, err)
t6, err := p6.GetIdentityToken("subject", "caURL")
assert.FatalError(t, err)
t7, err := p6.GetIdentityToken("subject", "caURL")
assert.FatalError(t, err)
t8, err := p6.GetIdentityToken("subject", "caURL")
assert.FatalError(t, err)
t11, err := generateAzureToken("subject", p1.oidcConfig.Issuer, azureDefaultAudience, t11, err := generateAzureToken("subject", p1.oidcConfig.Issuer, azureDefaultAudience,
p1.TenantID, "subscriptionID", "resourceGroup", "virtualMachine", p1.TenantID, "subscriptionID", "resourceGroup", "virtualMachine",
@ -434,8 +476,12 @@ func TestAzure_AuthorizeSign(t *testing.T) {
{"ok", p1, args{t1}, 5, http.StatusOK, false}, {"ok", p1, args{t1}, 5, http.StatusOK, false},
{"ok", p2, args{t2}, 10, http.StatusOK, false}, {"ok", p2, args{t2}, 10, http.StatusOK, false},
{"ok", p1, args{t11}, 5, http.StatusOK, false}, {"ok", p1, args{t11}, 5, http.StatusOK, false},
{"ok", p5, args{t5}, 5, http.StatusOK, false},
{"ok", p7, args{t7}, 5, http.StatusOK, false},
{"fail tenant", p3, args{t3}, 0, http.StatusUnauthorized, true}, {"fail tenant", p3, args{t3}, 0, http.StatusUnauthorized, true},
{"fail resource group", p4, args{t4}, 0, http.StatusUnauthorized, true}, {"fail resource group", p4, args{t4}, 0, http.StatusUnauthorized, true},
{"fail subscription", p6, args{t6}, 0, http.StatusUnauthorized, true},
{"fail object id", p8, args{t8}, 0, http.StatusUnauthorized, true},
{"fail token", p1, args{"token"}, 0, http.StatusUnauthorized, true}, {"fail token", p1, args{"token"}, 0, http.StatusUnauthorized, true},
{"fail issuer", p1, args{failIssuer}, 0, http.StatusUnauthorized, true}, {"fail issuer", p1, args{failIssuer}, 0, http.StatusUnauthorized, true},
{"fail audience", p1, args{failAudience}, 0, http.StatusUnauthorized, true}, {"fail audience", p1, args{failAudience}, 0, http.StatusUnauthorized, true},

View file

@ -710,6 +710,8 @@ func ProvisionerToCertificates(p *linkedca.Provisioner) (provisioner.Interface,
Name: p.Name, Name: p.Name,
TenantID: cfg.TenantId, TenantID: cfg.TenantId,
ResourceGroups: cfg.ResourceGroups, ResourceGroups: cfg.ResourceGroups,
SubscriptionIDs: cfg.SubscriptionIds,
ObjectIDs: cfg.ObjectIds,
Audience: cfg.Audience, Audience: cfg.Audience,
DisableCustomSANs: cfg.DisableCustomSans, DisableCustomSANs: cfg.DisableCustomSans,
DisableTrustOnFirstUse: cfg.DisableTrustOnFirstUse, DisableTrustOnFirstUse: cfg.DisableTrustOnFirstUse,
@ -869,6 +871,8 @@ func ProvisionerToLinkedca(p provisioner.Interface) (*linkedca.Provisioner, erro
Azure: &linkedca.AzureProvisioner{ Azure: &linkedca.AzureProvisioner{
TenantId: p.TenantID, TenantId: p.TenantID,
ResourceGroups: p.ResourceGroups, ResourceGroups: p.ResourceGroups,
SubscriptionIds: p.SubscriptionIDs,
ObjectIds: p.ObjectIDs,
Audience: p.Audience, Audience: p.Audience,
DisableCustomSans: p.DisableCustomSANs, DisableCustomSans: p.DisableCustomSANs,
DisableTrustOnFirstUse: p.DisableTrustOnFirstUse, DisableTrustOnFirstUse: p.DisableTrustOnFirstUse,

2
go.mod
View file

@ -35,7 +35,7 @@ require (
go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352 go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352
go.step.sm/cli-utils v0.7.0 go.step.sm/cli-utils v0.7.0
go.step.sm/crypto v0.15.0 go.step.sm/crypto v0.15.0
go.step.sm/linkedca v0.9.2 go.step.sm/linkedca v0.10.0
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3 golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3
golang.org/x/net v0.0.0-20220114011407-0dd24b26b47d golang.org/x/net v0.0.0-20220114011407-0dd24b26b47d
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9 // indirect golang.org/x/sys v0.0.0-20220114195835-da31bd327af9 // indirect

2
go.sum
View file

@ -687,6 +687,8 @@ go.step.sm/crypto v0.15.0 h1:VioBln+x3+RoejgeBhvxkLGVYdWRy6PFiAaUUN29/E0=
go.step.sm/crypto v0.15.0/go.mod h1:3G0yQr5lQqfEG0CMYz8apC/qMtjLRQlzflL2AxkcN+g= go.step.sm/crypto v0.15.0/go.mod h1:3G0yQr5lQqfEG0CMYz8apC/qMtjLRQlzflL2AxkcN+g=
go.step.sm/linkedca v0.9.2 h1:CpAkd174sLXFfrOZrbPEiTzik91QRj3+L0omsiwsiok= go.step.sm/linkedca v0.9.2 h1:CpAkd174sLXFfrOZrbPEiTzik91QRj3+L0omsiwsiok=
go.step.sm/linkedca v0.9.2/go.mod h1:5uTRjozEGSPAZal9xJqlaD38cvJcLe3o1VAFVjqcORo= go.step.sm/linkedca v0.9.2/go.mod h1:5uTRjozEGSPAZal9xJqlaD38cvJcLe3o1VAFVjqcORo=
go.step.sm/linkedca v0.10.0 h1:+bqymMRulHYkVde4l16FnqFVskoS6HCWJN5Z5cxAqF8=
go.step.sm/linkedca v0.10.0/go.mod h1:5uTRjozEGSPAZal9xJqlaD38cvJcLe3o1VAFVjqcORo=
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=