diff --git a/authority/provisioner/azure.go b/authority/provisioner/azure.go index 55d77f49b..14e944109 100644 --- a/authority/provisioner/azure.go +++ b/authority/provisioner/azure.go @@ -89,6 +89,8 @@ type Azure struct { Name string `json:"name"` TenantID string `json:"tenantID"` ResourceGroups []string `json:"resourceGroups"` + SubscriptionIDs []string `json:"subscriptionIDs"` + ObjectIDs []string `json:"ObjectIDs"` Audience string `json:"audience,omitempty"` DisableCustomSANs bool `json:"disableCustomSANs"` DisableTrustOnFirstUse bool `json:"disableTrustOnFirstUse"` @@ -224,14 +226,14 @@ func (p *Azure) Init(config Config) (err error) { return nil } -// authorizeToken returns the claims, name, group, error. -func (p *Azure) authorizeToken(token string) (*azurePayload, string, string, error) { +// authorizeToken returns the claims, name, group, subscription, identityObjectID, error. +func (p *Azure) authorizeToken(token string) (*azurePayload, string, string, string, string, error) { jwt, err := jose.ParseSigned(token) 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 { - return nil, "", "", errs.Unauthorized("azure.authorizeToken; azure token missing header") + return nil, "", "", "", "", errs.Unauthorized("azure.authorizeToken; azure token missing header") } var found bool @@ -244,7 +246,7 @@ func (p *Azure) authorizeToken(token string) (*azurePayload, string, string, err } } 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{ @@ -252,26 +254,27 @@ func (p *Azure) authorizeToken(token string) (*azurePayload, string, string, err Issuer: p.oidcConfig.Issuer, Time: time.Now(), }, 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 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) 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] - return &claims, name, group, nil + identityObjectID := claims.ObjectID + 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 // will be used on certificate creation. 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 { 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 data := x509util.NewTemplateData() 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()) } - _, name, _, err := p.authorizeToken(token) + _, name, _, _, _, err := p.authorizeToken(token) if err != nil { return nil, errs.Wrap(http.StatusInternalServerError, err, "azure.AuthorizeSSHSign") } diff --git a/authority/provisioner/azure_test.go b/authority/provisioner/azure_test.go index 7f8d60175..4ab734d54 100644 --- a/authority/provisioner/azure_test.go +++ b/authority/provisioner/azure_test.go @@ -333,7 +333,7 @@ func TestAzure_authorizeToken(t *testing.T) { for name, tt := range tests { t.Run(name, func(t *testing.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) { sc, ok := err.(errs.StatusCoder) 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, 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.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() assert.FatalError(t, err) @@ -393,6 +427,14 @@ func TestAzure_AuthorizeSign(t *testing.T) { assert.FatalError(t, err) t4, err := p4.GetIdentityToken("subject", "caURL") 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, p1.TenantID, "subscriptionID", "resourceGroup", "virtualMachine", @@ -434,8 +476,12 @@ func TestAzure_AuthorizeSign(t *testing.T) { {"ok", p1, args{t1}, 5, http.StatusOK, false}, {"ok", p2, args{t2}, 10, 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 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 issuer", p1, args{failIssuer}, 0, http.StatusUnauthorized, true}, {"fail audience", p1, args{failAudience}, 0, http.StatusUnauthorized, true}, diff --git a/authority/provisioners.go b/authority/provisioners.go index f8e7f4efc..8dc27c6a1 100644 --- a/authority/provisioners.go +++ b/authority/provisioners.go @@ -710,6 +710,8 @@ func ProvisionerToCertificates(p *linkedca.Provisioner) (provisioner.Interface, Name: p.Name, TenantID: cfg.TenantId, ResourceGroups: cfg.ResourceGroups, + SubscriptionIDs: cfg.SubscriptionIds, + ObjectIDs: cfg.ObjectIds, Audience: cfg.Audience, DisableCustomSANs: cfg.DisableCustomSans, DisableTrustOnFirstUse: cfg.DisableTrustOnFirstUse, @@ -869,6 +871,8 @@ func ProvisionerToLinkedca(p provisioner.Interface) (*linkedca.Provisioner, erro Azure: &linkedca.AzureProvisioner{ TenantId: p.TenantID, ResourceGroups: p.ResourceGroups, + SubscriptionIds: p.SubscriptionIDs, + ObjectIds: p.ObjectIDs, Audience: p.Audience, DisableCustomSans: p.DisableCustomSANs, DisableTrustOnFirstUse: p.DisableTrustOnFirstUse, diff --git a/go.mod b/go.mod index 4e22ad87f..46fe260c7 100644 --- a/go.mod +++ b/go.mod @@ -35,7 +35,7 @@ require ( go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352 go.step.sm/cli-utils v0.7.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/net v0.0.0-20220114011407-0dd24b26b47d golang.org/x/sys v0.0.0-20220114195835-da31bd327af9 // indirect diff --git a/go.sum b/go.sum index 886074321..1cd8e2e75 100644 --- a/go.sum +++ b/go.sum @@ -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/linkedca v0.9.2 h1:CpAkd174sLXFfrOZrbPEiTzik91QRj3+L0omsiwsiok= 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.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=