Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -47,16 +47,16 @@ help: ## Display this help.

.PHONY: manifests
manifests: $(CONTROLLER_GEN) ## Generate WebhookConfiguration, ClusterRole and CustomResourceDefinition objects.
$(CONTROLLER_GEN) rbac:roleName=acrpull-controller crd paths="./..." output:dir=config/helm/templates output:crd:artifacts:config=config/helm/templates
$(shell cygpath -u "$(GOBIN)/controller-gen-v0.14.0") rbac:roleName=acrpull-controller crd paths="./api/v1beta1" paths="./api/v1beta2" paths="./internal/controller" output:dir=config/helm/templates output:crd:artifacts:config=config/helm/templates
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cygpath is only present in Cygwin installations. Shouldn't this be more portable in case its used in WSL etc?

mv config/helm/templates/role.yaml config/helm/templates/controller_role.yaml

.PHONY: generate
generate: $(CONTROLLER_GEN) mocks ## Generate code containing DeepCopy, DeepCopyInto, and DeepCopyObject method implementations.
$(CONTROLLER_GEN) object:headerFile="hack/boilerplate.go.txt" paths="./..."
$(shell cygpath -u "$(GOBIN)/controller-gen-v0.14.0") object:headerFile="hack/boilerplate.go.txt" paths="./api/v1beta1" paths="./api/v1beta2"

.PHONY: mocks
mocks: $(MOCKGEN) ## Generate code containing DeepCopy, DeepCopyInto, and DeepCopyObject method implementations.
$(MOCKGEN) -source=pkg/authorizer/interfaces.go > pkg/authorizer/mock_authorizer/interfaces.go
mocks: ## Generate code containing DeepCopy, DeepCopyInto, and DeepCopyObject method implementations.
$(shell cygpath -u "$(GOBIN)/mockgen") -source=pkg/authorizer/interfaces.go > pkg/authorizer/mock_authorizer/interfaces.go

.PHONY: fmt
fmt: ## Run go fmt against code.
Expand Down
63 changes: 63 additions & 0 deletions api/v1beta2/acrpullbinding_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,11 @@ type AuthenticationMethod struct {

// WorkloadIdentity uses Azure Workload Identity to authenticate with Azure.
WorkloadIdentity *WorkloadIdentityAuth `json:"workloadIdentity,omitempty"`

// +kubebuilder:validation:Optional

// Federated Identity Creationals for Cross Cloud
FederatedIdentity *FederatedIdentityAuth `json:"federatedIdentity,omitempty"`
}

// +kubebuilder:validation:XValidation:rule="[has(self.clientID), has(self.resourceID)].exists_one(x, x)", message="only client or resource ID can be set"
Expand Down Expand Up @@ -171,6 +176,64 @@ type WorkloadIdentityAuth struct {
TenantID string `json:"tenantID,omitempty"`
}

// +kubebuilder:validation:XValidation:rule="(has(self.sourceClientID) && has(self.SourceTenantID))", message="custom client and tenant identifiers must be provided together, if at all"

type FederatedIdentityAuth struct {
// +kubebuilder:validation:Required
// +kubebuilder:example="1b461305-28be-5271-beda-bd9fd2e24251"

// SourceClienTid holds the identified of identity residing on the tenant where request has been made.
SourceClientID string `json:"sourceClientID,omitempty"`

// +kubebuilder:validation:Required
// +kubebuilder:example="72f988bf-86f1-41af-91ab-2d7cd011db47"

// TenantID holds an optional tenant identifier of a federated identity.
// Specify this identifier if multiple identities are federated with the
// service account and the identity to use for image pulling is not the
// default identity stored in the service account's annotations. The
// client and tenant ID must be specified together.
SourceTenantID string `json:"sourceTenantID,omitempty"`

// +kubebuilder:validation:Required
// +kubebuilder:example="1b461305-28be-5271-beda-bd9fd2e24251"

// ClientID holds an optional client identifier of a federated identity.
// Specify this identifier if multiple identities are federated with the
// service account and the identity to use for image pulling is not the
// default identity stored in the service account's annotations. The
// client and tenant ID must be specified together.
TargetClientID string `json:"targetClientID,omitempty"`

// +kubebuilder:validation:Required
// +kubebuilder:example="72f988bf-86f1-41af-91ab-2d7cd011db47"

// TenantID holds an optional tenant identifier of a federated identity.
// Specify this identifier if multiple identities are federated with the
// service account and the identity to use for image pulling is not the
// default identity stored in the service account's annotations. The
// client and tenant ID must be specified together.
TargetTenantID string `json:"targetTenantID,omitempty"`

// +kubebuilder:validation:Required
// +kubebuilder:example="72f988bf-86f1-41af-91ab-2d7cd011db47"

// TenantID holds an optional tenant identifier of a federated identity.
// Specify this identifier if multiple identities are federated with the
// service account and the identity to use for image pulling is not the
// default identity stored in the service account's annotations. The
// client and tenant ID must be specified together.
Scope string `json:"scope,omitempty"`
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Missing comment, it's same as for tenantid


// +kubebuilder:validation:Required
// +kubebuilder:validation:Enum=PublicCloud;USGovernmentCloud;ChinaCloud;AirgappedCloud
// +kubebuilder:default=PublicCloud
// +kubebuilder:example=PublicCloud

// Environment specifies the Azure Cloud environment in which the ACR is deployed.
TargetEnvironment AzureEnvironmentType `json:"targetEnvironment,omitempty"`
}

// AcrPullBindingStatus defines the observed state of AcrPullBinding
type AcrPullBindingStatus struct {
// +kubebuilder:validation:Optional
Expand Down
20 changes: 20 additions & 0 deletions api/v1beta2/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

60 changes: 60 additions & 0 deletions config/helm/templates/acrpull.microsoft.com_acrpullbindings.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,66 @@ spec:
description: Auth determines how we will authenticate to the Azure
Container Registry. Only one method may be provided.
properties:
federatedIdentity:
description: Federated Identity Creationals for Cross Cloud
properties:
scope:
description: |-
TenantID holds an optional tenant identifier of a federated identity.
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this description correct. This is the target scope right?

Specify this identifier if multiple identities are federated with the
service account and the identity to use for image pulling is not the
default identity stored in the service account's annotations. The
client and tenant ID must be specified together.
example: 72f988bf-86f1-41af-91ab-2d7cd011db47
type: string
sourceClientID:
description: SourceClienTid holds the identified of identity
residing on the tenant where request has been made.
example: 1b461305-28be-5271-beda-bd9fd2e24251
type: string
sourceTenantID:
description: |-
TenantID holds an optional tenant identifier of a federated identity.
Specify this identifier if multiple identities are federated with the
service account and the identity to use for image pulling is not the
default identity stored in the service account's annotations. The
client and tenant ID must be specified together.
example: 72f988bf-86f1-41af-91ab-2d7cd011db47
type: string
targetClientID:
description: |-
ClientID holds an optional client identifier of a federated identity.
Specify this identifier if multiple identities are federated with the
service account and the identity to use for image pulling is not the
default identity stored in the service account's annotations. The
client and tenant ID must be specified together.
example: 1b461305-28be-5271-beda-bd9fd2e24251
type: string
targetEnvironment:
default: PublicCloud
description: Environment specifies the Azure Cloud environment
in which the ACR is deployed.
enum:
- PublicCloud
- USGovernmentCloud
- ChinaCloud
- AirgappedCloud
example: PublicCloud
type: string
targetTenantID:
description: |-
TenantID holds an optional tenant identifier of a federated identity.
Specify this identifier if multiple identities are federated with the
service account and the identity to use for image pulling is not the
default identity stored in the service account's annotations. The
client and tenant ID must be specified together.
example: 72f988bf-86f1-41af-91ab-2d7cd011db47
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Better to make this AME or prdtrs01 right? Those are the only allowed combinations when originating from a ME/trs tenant.

type: string
type: object
x-kubernetes-validations:
- message: custom client and tenant identifiers must be provided
together, if at all
rule: (has(self.sourceClientID) && has(self.SourceTenantID))
managedIdentity:
description: ManagedIdentity uses Azure Managed Identity to authenticate
with Azure.
Expand Down
67 changes: 65 additions & 2 deletions internal/controller/acrpullbinding_v1beta2_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ import (
crclient "sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/handler"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/cloud"

)

type CoreOpts struct {
Expand Down Expand Up @@ -99,7 +103,27 @@ func NewV1beta2Reconciler(opts *V1beta2ReconcilerOpts) *PullBindingReconciler {
},
CreatePullCredential: func(ctx context.Context, binding *msiacrpullv1beta2.AcrPullBinding, serviceAccount *corev1.ServiceAccount) (string, time.Time, error) {
var tenantId, clientId, token string
if binding.Spec.Auth.WorkloadIdentity != nil {
if binding.Spec.Auth.FederatedIdentity != nil {
fmt.Printf("Using Federated Identity Credentials %s %s\n", binding.Spec.Auth.FederatedIdentity.SourceTenantID, binding.Spec.Auth.FederatedIdentity.SourceClientID)

tenantId = binding.Spec.Auth.FederatedIdentity.SourceTenantID
clientId = binding.Spec.Auth.FederatedIdentity.SourceClientID

msiCred, err := azidentity.NewDefaultAzureCredential(nil)

if err != nil {
return "", time.Time{}, fmt.Errorf("failed to get MSI token: %w", err)
}

msiToken, err := msiCred.GetToken(ctx, policy.TokenRequestOptions{Scopes: []string{binding.Spec.Auth.FederatedIdentity.Scope + "/.default"}})
fmt.Printf("MSI Token scope: %s\n", binding.Spec.Auth.FederatedIdentity.Scope )

if err != nil {
return "", time.Time{}, fmt.Errorf("Msi Token: %v", err)
}

token = msiToken.Token
} else if binding.Spec.Auth.WorkloadIdentity != nil {
if binding.Spec.Auth.WorkloadIdentity.TenantID != "" {
tenantId = binding.Spec.Auth.WorkloadIdentity.TenantID
clientId = binding.Spec.Auth.WorkloadIdentity.ClientID
Expand Down Expand Up @@ -131,7 +155,33 @@ func NewV1beta2Reconciler(opts *V1beta2ReconcilerOpts) *PullBindingReconciler {
return "", time.Time{}, fmt.Errorf("failed to retrieve ARM token: %v", err)
}

acrToken, err := opts.exchangeArmTokenForAcrToken(ctx, armToken, binding.Spec.ACR)
var exchangeToken azcore.AccessToken

if binding.Spec.Auth.FederatedIdentity != nil {
env := environment(binding.Spec.Auth.FederatedIdentity.TargetEnvironment)

assertionFunc := func(ctx context.Context) (string, error) {
return armToken.Token, nil
}

// Token 2
ficCred, err := azidentity.NewClientAssertionCredential(binding.Spec.Auth.FederatedIdentity.TargetTenantID, binding.Spec.Auth.FederatedIdentity.TargetClientID, assertionFunc, nil)
if err != nil {
return "", time.Time{}, fmt.Errorf("WW Token: %v", err)
}

ficToken, err := ficCred.GetToken(ctx, policy.TokenRequestOptions{Scopes: []string{ env.Services[cloud.ResourceManager].Audience + "/.default"}})
if err != nil {
return "", time.Time{}, fmt.Errorf("WW Token: %v", err)
}

exchangeToken = ficToken

} else {
exchangeToken = armToken
}

acrToken, err := opts.exchangeArmTokenForAcrToken(ctx, exchangeToken, binding.Spec.ACR)
if err != nil {
return "", time.Time{}, fmt.Errorf("failed to retrieve ACR token: %v", err)
}
Expand Down Expand Up @@ -251,3 +301,16 @@ func refreshBoundary(refresh, expiry time.Time, ttlRotationFraction float64) tim
func needsRefresh(now func() time.Time, refresh, expiry time.Time, ttlRotationFraction float64) bool {
return now().After(refreshBoundary(refresh, expiry, ttlRotationFraction))
}

func environment(input msiacrpullv1beta2.AzureEnvironmentType) cloud.Configuration {
switch input {
case msiacrpullv1beta2.AzureEnvironmentPublicCloud:
return cloud.AzurePublic
case msiacrpullv1beta2.AzureEnvironmentUSGovernmentCloud:
return cloud.AzureGovernment
case msiacrpullv1beta2.AzureEnvironmentChinaCloud:
return cloud.AzureChina
default:
panic(fmt.Errorf("unsupported msiacrpullv1beta2.AzureEnvironmentType: %s", input))
}
}
9 changes: 2 additions & 7 deletions pkg/authorizer/mock_authorizer/interfaces.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 13 additions & 2 deletions pkg/authorizer/token_retriever.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
"github.com/Azure/azure-sdk-for-go/sdk/azidentity"

ctrl "sigs.k8s.io/controller-runtime"

Check failure on line 13 in pkg/authorizer/token_retriever.go

View workflow job for this annotation

GitHub Actions / Build

"sigs.k8s.io/controller-runtime" imported as ctrl and not used) (typecheck)

Check failure on line 13 in pkg/authorizer/token_retriever.go

View workflow job for this annotation

GitHub Actions / Build

"sigs.k8s.io/controller-runtime" imported as ctrl and not used) (typecheck)

Check failure on line 13 in pkg/authorizer/token_retriever.go

View workflow job for this annotation

GitHub Actions / Build

"sigs.k8s.io/controller-runtime" imported as ctrl and not used (typecheck)

msiacrpullv1beta2 "github.com/Azure/msi-acrpull/api/v1beta2"
)

Expand All @@ -36,6 +38,7 @@

var credential azcore.TokenCredential
var err error

switch {
case spec.Auth.ManagedIdentity != nil:
var id azidentity.ManagedIDKind
Expand All @@ -45,7 +48,7 @@
id = azidentity.ResourceID(spec.Auth.ManagedIdentity.ResourceID)
}
credential, err = azidentity.NewManagedIdentityCredential(&azidentity.ManagedIdentityCredentialOptions{ID: id})
case spec.Auth.WorkloadIdentity != nil:
case spec.Auth.WorkloadIdentity != nil || spec.Auth.FederatedIdentity != nil:
// n.b. the built-in azidentity.WorkloadIdentityCredential assumes we're loading a service account token
// from a file in a Pod, where the Kubernetes API server is rotating it, etc. Unfortunately that is not
// our use-case here, and we certainly don't want to centralize every service account token we ever mint
Expand All @@ -68,7 +71,15 @@
// this should never happen with the validation we have on the CRD
panic(fmt.Errorf("programmer error: ACRPullBinding.Spec.Auth has no method: %#v", spec.Auth))
}
return credential.GetToken(ctx, policy.TokenRequestOptions{Scopes: []string{env.Services[cloud.ResourceManager].Audience + "/.default"}})

// Default Audience to the Resource Manager audience for the environment
var audience string = env.Services[cloud.ResourceManager].Audience

if spec.Auth.FederatedIdentity != nil {
audience = "api://AzureADTokenExchange"
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Feel this should be switched based on the target environment.

}

return credential.GetToken(ctx, policy.TokenRequestOptions{Scopes: []string{audience + "/.default"}})
}

func environment(input msiacrpullv1beta2.AzureEnvironmentType, config *msiacrpullv1beta2.AirgappedCloudConfiguration) cloud.Configuration {
Expand Down
Loading