From 772131a6d0782b8d7f69ac5d3be33cab673e468e Mon Sep 17 00:00:00 2001 From: Dipti Pai Date: Thu, 22 Aug 2024 22:41:55 -0700 Subject: [PATCH] Enable Azure OIDC for Azure DevOps Respository - Add a new provider field to GitRepository API spec which can be set to azure to enable passwordless authentication to Azure DevOps repositories. - API docs for new provider field and guidance to setup Azure environment with workload identity. - Controller changes to set the provider options in git authoptions to fetch credential while cloning the repository. - Add unit tests for testing provider Signed-off-by: Dipti Pai --- api/v1/gitrepository_types.go | 15 ++++ ...rce.toolkit.fluxcd.io_gitrepositories.yaml | 9 ++ docs/api/v1/source.md | 26 ++++++ docs/spec/v1/gitrepositories.md | 85 +++++++++++++++++++ go.mod | 7 ++ go.sum | 10 ++- .../controller/gitrepository_controller.go | 14 +++ .../gitrepository_controller_test.go | 48 +++++++++++ 8 files changed, 210 insertions(+), 4 deletions(-) diff --git a/api/v1/gitrepository_types.go b/api/v1/gitrepository_types.go index 0d3b3abea..bfe309871 100644 --- a/api/v1/gitrepository_types.go +++ b/api/v1/gitrepository_types.go @@ -27,6 +27,14 @@ import ( const ( // GitRepositoryKind is the string representation of a GitRepository. GitRepositoryKind = "GitRepository" + + // GitProviderGeneric provides support for authentication using + // credentials specified in secretRef. + GitProviderGeneric string = "generic" + + // GitProviderAzure provides support for authentication to azure + // repositories using Managed Identity. + GitProviderAzure string = "azure" ) const ( @@ -80,6 +88,13 @@ type GitRepositorySpec struct { // +optional SecretRef *meta.LocalObjectReference `json:"secretRef,omitempty"` + // Provider used for authentication, can be 'azure', 'generic'. + // When not specified, defaults to 'generic'. + // +kubebuilder:validation:Enum=generic;azure + // +kubebuilder:default:=generic + // +optional + Provider string `json:"provider,omitempty"` + // Interval at which the GitRepository URL is checked for updates. // This interval is approximate and may be subject to jitter to ensure // efficient use of resources. diff --git a/config/crd/bases/source.toolkit.fluxcd.io_gitrepositories.yaml b/config/crd/bases/source.toolkit.fluxcd.io_gitrepositories.yaml index f12533ad6..e7a48ee25 100644 --- a/config/crd/bases/source.toolkit.fluxcd.io_gitrepositories.yaml +++ b/config/crd/bases/source.toolkit.fluxcd.io_gitrepositories.yaml @@ -103,6 +103,15 @@ spec: efficient use of resources. pattern: ^([0-9]+(\.[0-9]+)?(ms|s|m|h))+$ type: string + provider: + default: generic + description: |- + Provider used for authentication, can be 'azure', 'generic'. + When not specified, defaults to 'generic'. + enum: + - generic + - azure + type: string proxySecretRef: description: |- ProxySecretRef specifies the Secret containing the proxy configuration diff --git a/docs/api/v1/source.md b/docs/api/v1/source.md index 1424cdecc..521dddc14 100644 --- a/docs/api/v1/source.md +++ b/docs/api/v1/source.md @@ -383,6 +383,19 @@ and ‘known_hosts’ fields.

+provider
+ +string + + + +(Optional) +

Provider used for authentication, can be ‘azure’, ‘generic’. +When not specified, defaults to ‘generic’.

+ + + + interval
@@ -1710,6 +1723,19 @@ and ‘known_hosts’ fields.

+provider
+ +string + + + +(Optional) +

Provider used for authentication, can be ‘azure’, ‘generic’. +When not specified, defaults to ‘generic’.

+ + + + interval
diff --git a/docs/spec/v1/gitrepositories.md b/docs/spec/v1/gitrepositories.md index 4170d9f1b..e78aee74a 100644 --- a/docs/spec/v1/gitrepositories.md +++ b/docs/spec/v1/gitrepositories.md @@ -212,6 +212,91 @@ For password-protected SSH private keys, the password must be provided via an additional `password` field in the secret. Flux CLI also supports this via the `--password` flag. +### Provider + +`.spec.provider` is an optional field that allows specifying an OIDC provider +used for authentication purposes. + +Supported options are: + +- `generic` +- `azure` + +When provider is not specified, it defaults to `generic` indicating that +mechanisms using `spec.secretRef` are used for authentication. + +#### Azure + +The `azure` provider can be used to authenticate to Azure DevOps repositories +automatically using Workload Identity. + +##### Pre-requisites + +- Ensure that your Azure DevOps Organization is + [connected](https://learn.microsoft.com/en-us/azure/devops/organizations/accounts/connect-organization-to-azure-ad?view=azure-devops) + to Microsoft Entra. +- Ensure Workload Identity is properly [set up on your + cluster](https://learn.microsoft.com/en-us/azure/aks/workload-identity-deploy-cluster#create-an-aks-cluster). + +##### Configure Flux controller + +- Create a managed identity to access Azure DevOps. Establish a federated + identity credential between the managed identity and the source-controller + service account. In the default installation, the source-controller service + account is located in the `flux-system` namespace with name + `source-controller`. Ensure the federated credential uses the correct + namespace and name of the source-controller service account. For more details, + please refer to this + [guide](https://azure.github.io/azure-workload-identity/docs/quick-start.html#6-establish-federated-identity-credential-between-the-identity-and-the-service-account-issuer--subject). + +- Add the managed identity to the Azure DevOps organization as a user. Ensure + that the managed identity has the necessary permissions to access the Azure + DevOps repository as described + [here](https://learn.microsoft.com/en-us/azure/devops/integrate/get-started/authentication/service-principal-managed-identity?view=azure-devops#2-add-and-manage-service-principals-in-an-azure-devops-organization). + +- Add the following patch to your bootstrap repository in + `flux-system/kustomization.yaml` file: + + +```yaml +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - gotk-components.yaml + - gotk-sync.yaml +patches: + - patch: |- + apiVersion: v1 + kind: ServiceAccount + metadata: + name: source-controller + namespace: flux-system + annotations: + azure.workload.identity/client-id: + labels: + azure.workload.identity/use: "true" + - patch: |- + apiVersion: apps/v1 + kind: Deployment + metadata: + name: source-controller + namespace: flux-system + labels: + azure.workload.identity/use: "true" + spec: + template: + metadata: + labels: + azure.workload.identity/use: "true" +``` + +**Note:** When azure `provider` is used with `GitRepository`, the `.spec.url` +must follow this format: + +``` +https://dev.azure.com/{your-organization}/{your-project}/_git/{your-repository} +``` + ### Interval `.spec.interval` is a required field that specifies the interval at which the diff --git a/go.mod b/go.mod index 9386f8588..4d8d4d7da 100644 --- a/go.mod +++ b/go.mod @@ -24,6 +24,7 @@ require ( github.com/fluxcd/cli-utils v0.36.0-flux.9 github.com/fluxcd/pkg/apis/event v0.10.0 github.com/fluxcd/pkg/apis/meta v1.6.0 + github.com/fluxcd/pkg/auth v0.0.0-00010101000000-000000000000 github.com/fluxcd/pkg/git v0.20.0 github.com/fluxcd/pkg/git/gogit v0.20.0 github.com/fluxcd/pkg/gittestserver v0.13.0 @@ -406,3 +407,9 @@ require ( ) retract v0.32.0 // Refers to incorrect ./api version. + +replace github.com/fluxcd/pkg/auth => github.com/dipti-pai/pkg/auth v0.0.0-20240910203859-abee735aa028 + +replace github.com/fluxcd/pkg/git/gogit => github.com/dipti-pai/pkg/git/gogit v0.0.0-20240910203859-abee735aa028 + +replace github.com/fluxcd/pkg/git => github.com/dipti-pai/pkg/git v0.0.0-20240910203859-abee735aa028 diff --git a/go.sum b/go.sum index fea630678..a2c1f6244 100644 --- a/go.sum +++ b/go.sum @@ -291,6 +291,12 @@ github.com/digitorus/timestamp v0.0.0-20231217203849-220c5c2851b7 h1:lxmTCgmHE1G github.com/digitorus/timestamp v0.0.0-20231217203849-220c5c2851b7/go.mod h1:GvWntX9qiTlOud0WkQ6ewFm0LPy5JUR1Xo0Ngbd1w6Y= github.com/dimchansky/utfbom v1.1.1 h1:vV6w1AhK4VMnhBno/TPVCoK9U/LP0PkLCS9tbxHdi/U= github.com/dimchansky/utfbom v1.1.1/go.mod h1:SxdoEBH5qIqFocHMyGOXVAybYJdr71b1Q/j0mACtrfE= +github.com/dipti-pai/pkg/auth v0.0.0-20240910203859-abee735aa028 h1:Ohfv7mzT0aecvS4DJqDgBCGiKccsH8YcfOJ1fwdWi5g= +github.com/dipti-pai/pkg/auth v0.0.0-20240910203859-abee735aa028/go.mod h1:0VS8EHPXNoB9q84OJg+t2LlkdIvWzttUPXhSxMKavGk= +github.com/dipti-pai/pkg/git v0.0.0-20240910203859-abee735aa028 h1:H9PpGshNFcO5yenhJDJOHXF6x5jgof64YI5l+AYkpEQ= +github.com/dipti-pai/pkg/git v0.0.0-20240910203859-abee735aa028/go.mod h1:XTZfxHFy96sbGzbhN68u8+L6IKjqAxLax/dCq9gaUk4= +github.com/dipti-pai/pkg/git/gogit v0.0.0-20240910203859-abee735aa028 h1:Ekj1aPhfud5phbZq9rwZeN43YE/IL1RzFghoRDJKC6I= +github.com/dipti-pai/pkg/git/gogit v0.0.0-20240910203859-abee735aa028/go.mod h1:pX0wDKVhNINddJ3vtUS6ripizHTqjc+kk93CLO0UDmM= github.com/distribution/distribution/v3 v3.0.0-beta.1 h1:X+ELTxPuZ1Xe5MsD3kp2wfGUhc8I+MPfRis8dZ818Ic= github.com/distribution/distribution/v3 v3.0.0-beta.1/go.mod h1:O9O8uamhHzWWQVTjuQpyYUVm/ShPHPUDgvQMpHGVBDs= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= @@ -351,10 +357,6 @@ github.com/fluxcd/pkg/apis/meta v1.6.0 h1:93TcRpiph0OCoQh+cI+PM7E35kBW9dScuas9tW github.com/fluxcd/pkg/apis/meta v1.6.0/go.mod h1:ZOeHcvyVdZDC5ZOGV7YuwplIvAx6LvmpeyhfTcNZCnc= github.com/fluxcd/pkg/cache v0.0.3 h1:VK5joG/p+amh5Ob+r1OFOx0cCYiswEf8mX1/J1BG7Mw= github.com/fluxcd/pkg/cache v0.0.3/go.mod h1:UU6oFhV+mG0A5/RwIlvXhyuKlJwQEkk92jVB3vKMLtk= -github.com/fluxcd/pkg/git v0.20.0 h1:byUbxLLZ9AyVYmK16mvxY/iA/ZhNwA30GHKPKNh7pik= -github.com/fluxcd/pkg/git v0.20.0/go.mod h1:YnBOFhX7zzyVjg/u1Et1xBqXs30kb2sWWesIl3/glhw= -github.com/fluxcd/pkg/git/gogit v0.20.0 h1:ZlWq//I465lv9aEEWaJhjJaTiTtnjcH+Td0fg1rPXWU= -github.com/fluxcd/pkg/git/gogit v0.20.0/go.mod h1:ZA4WsKr28cj1yuplxOw9vHgCL4OCNJJLib1cJ77Tp9o= github.com/fluxcd/pkg/gittestserver v0.13.0 h1:6rvD9Z7+4zBcNT+LK0z4H0z6mDaw1Zd8ZaLh/dw8dzI= github.com/fluxcd/pkg/gittestserver v0.13.0/go.mod h1:LDw32Wo9mTmKNmJq4g7LRVBqPXlpMIWFBDOrRRh/+As= github.com/fluxcd/pkg/helmtestserver v0.19.0 h1:DbidD46we8iLp/Sxn2TO8twtlP5gxFQaP3XTNJC0bl8= diff --git a/internal/controller/gitrepository_controller.go b/internal/controller/gitrepository_controller.go index aadbb6267..39e43fa9d 100644 --- a/internal/controller/gitrepository_controller.go +++ b/internal/controller/gitrepository_controller.go @@ -27,6 +27,7 @@ import ( "time" securejoin "github.com/cyphar/filepath-securejoin" + "github.com/fluxcd/pkg/auth/azure" "github.com/fluxcd/pkg/runtime/logger" "github.com/go-git/go-git/v5/plumbing/transport" corev1 "k8s.io/api/core/v1" @@ -647,6 +648,19 @@ func (r *GitRepositoryReconciler) getAuthOpts(ctx context.Context, obj *sourcev1 if err != nil { return nil, err } + + // Configure provider authentication if specified in spec + if obj.Spec.Provider != "" && obj.Spec.Provider != sourcev1.GitProviderGeneric { + if obj.Spec.Provider == sourcev1.GitProviderAzure { + authOpts.ProviderOpts = &git.ProviderOptions{ + Name: obj.Spec.Provider, + AzureOpts: []azure.OptFunc{ + azure.WithAzureDevOpsScope(), + }, + } + } + } + return authOpts, nil } diff --git a/internal/controller/gitrepository_controller_test.go b/internal/controller/gitrepository_controller_test.go index 800c65577..a81235553 100644 --- a/internal/controller/gitrepository_controller_test.go +++ b/internal/controller/gitrepository_controller_test.go @@ -683,6 +683,54 @@ func TestGitRepositoryReconciler_reconcileSource_authStrategy(t *testing.T) { } } +func TestGitRepositoryReconciler_getAuthOpts_provider(t *testing.T) { + tests := []struct { + name string + beforeFunc func(obj *sourcev1.GitRepository) + wantProviderOptsName string + }{ + { + name: "azure provider", + beforeFunc: func(obj *sourcev1.GitRepository) { + obj.Spec.Provider = sourcev1.GitProviderAzure + }, + wantProviderOptsName: sourcev1.GitProviderAzure, + }, + { + name: "generic provider", + beforeFunc: func(obj *sourcev1.GitRepository) { + obj.Spec.Provider = sourcev1.GitProviderGeneric + }, + }, + { + name: "no provider", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := NewWithT(t) + obj := &sourcev1.GitRepository{} + r := &GitRepositoryReconciler{} + url, _ := url.Parse("https://dev.azure.com/foo/bar/_git/baz") + + if tt.beforeFunc != nil { + tt.beforeFunc(obj) + } + opts, err := r.getAuthOpts(context.TODO(), obj, *url) + + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(opts).ToNot(BeNil()) + if tt.wantProviderOptsName != "" { + g.Expect(opts.ProviderOpts).ToNot(BeNil()) + g.Expect(opts.ProviderOpts.Name).To(Equal(tt.wantProviderOptsName)) + } else { + g.Expect(opts.ProviderOpts).To(BeNil()) + } + }) + } +} + func TestGitRepositoryReconciler_reconcileSource_checkoutStrategy(t *testing.T) { g := NewWithT(t)