| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376 |
- /*
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
- http://www.apache.org/licenses/LICENSE-2.0
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
- */
- package acr
- import (
- "context"
- "errors"
- "fmt"
- "io"
- "net/http"
- "net/url"
- "os"
- "strings"
- "github.com/Azure/azure-sdk-for-go/sdk/azcore"
- "github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
- "github.com/Azure/azure-sdk-for-go/sdk/azidentity"
- "github.com/Azure/go-autorest/autorest/azure"
- corev1 "k8s.io/api/core/v1"
- apiextensions "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
- "k8s.io/apimachinery/pkg/types"
- "k8s.io/apimachinery/pkg/util/json"
- "k8s.io/client-go/kubernetes"
- kcorev1 "k8s.io/client-go/kubernetes/typed/core/v1"
- "sigs.k8s.io/controller-runtime/pkg/client"
- ctrlcfg "sigs.k8s.io/controller-runtime/pkg/client/config"
- "sigs.k8s.io/yaml"
- "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
- genv1alpha1 "github.com/external-secrets/external-secrets/apis/generators/v1alpha1"
- smmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
- "github.com/external-secrets/external-secrets/pkg/provider/azure/keyvault"
- )
- type Generator struct {
- clientSecretCreds clientSecretCredentialFunc
- }
- type clientSecretCredentialFunc func(tenantID string, clientID string, clientSecret string, options *azidentity.ClientSecretCredentialOptions) (TokenGetter, error)
- type TokenGetter interface {
- GetToken(ctx context.Context, opts policy.TokenRequestOptions) (azcore.AccessToken, error)
- }
- const (
- defaultLoginUsername = "00000000-0000-0000-0000-000000000000"
- errNoSpec = "no config spec provided"
- errParseSpec = "unable to parse spec: %w"
- errCreateSess = "unable to create aws session: %w"
- errGetToken = "unable to get authorization token: %w"
- )
- // Generate generates a token that can be used to authenticate against Azure Container Registry.
- // First, an Azure Active Directory access token is obtained with the desired authentication method.
- // This AAD access token will be used to authenticate against ACR.
- // Depending on the generator spec it generates an ACR access token or an ACR refresh token.
- // * access tokens are scoped to a specific repository or action (pull,push)
- // * refresh tokens can are scoped to whatever policy is attached to the identity that creates the acr refresh token
- // details can be found here: https://github.com/Azure/acr/blob/main/docs/AAD-OAuth.md#overview
- func (g *Generator) Generate(ctx context.Context, jsonSpec *apiextensions.JSON, crClient client.Client, namespace string) (map[string][]byte, error) {
- cfg, err := ctrlcfg.GetConfig()
- if err != nil {
- return nil, err
- }
- kubeClient, err := kubernetes.NewForConfig(cfg)
- if err != nil {
- return nil, err
- }
- g.clientSecretCreds = func(tenantID, clientID, clientSecret string, options *azidentity.ClientSecretCredentialOptions) (TokenGetter, error) {
- return azidentity.NewClientSecretCredential(tenantID, clientID, clientSecret, options)
- }
- return g.generate(
- ctx,
- jsonSpec,
- crClient,
- namespace,
- kubeClient,
- fetchACRAccessToken,
- fetchACRRefreshToken)
- }
- func (g *Generator) generate(
- ctx context.Context,
- jsonSpec *apiextensions.JSON,
- crClient client.Client,
- namespace string,
- kubeClient kubernetes.Interface,
- fetchAccessToken accessTokenFetcher,
- fetchRefreshToken refreshTokenFetcher) (map[string][]byte, error) {
- if jsonSpec == nil {
- return nil, fmt.Errorf(errNoSpec)
- }
- res, err := parseSpec(jsonSpec.Raw)
- if err != nil {
- return nil, fmt.Errorf(errParseSpec, err)
- }
- var accessToken string
- // pick authentication strategy to create an AAD access token
- if res.Spec.Auth.ServicePrincipal != nil {
- accessToken, err = g.accessTokenForServicePrincipal(
- ctx,
- crClient,
- namespace,
- res.Spec.EnvironmentType,
- res.Spec.TenantID,
- res.Spec.Auth.ServicePrincipal.SecretRef.ClientID,
- res.Spec.Auth.ServicePrincipal.SecretRef.ClientSecret,
- )
- } else if res.Spec.Auth.ManagedIdentity != nil {
- accessToken, err = accessTokenForManagedIdentity(
- ctx,
- res.Spec.EnvironmentType,
- res.Spec.Auth.ManagedIdentity.IdentityID,
- )
- } else if res.Spec.Auth.WorkloadIdentity != nil {
- accessToken, err = accessTokenForWorkloadIdentity(
- ctx,
- crClient,
- kubeClient.CoreV1(),
- res.Spec.EnvironmentType,
- res.Spec.Auth.WorkloadIdentity.ServiceAccountRef,
- namespace,
- )
- } else {
- return nil, fmt.Errorf("unexpeted configuration")
- }
- if err != nil {
- return nil, err
- }
- var acrToken string
- acrToken, err = fetchRefreshToken(accessToken, res.Spec.TenantID, res.Spec.ACRRegistry)
- if err != nil {
- return nil, err
- }
- if res.Spec.Scope != "" {
- acrToken, err = fetchAccessToken(acrToken, res.Spec.TenantID, res.Spec.ACRRegistry, res.Spec.Scope)
- if err != nil {
- return nil, err
- }
- }
- return map[string][]byte{
- "username": []byte(defaultLoginUsername),
- "password": []byte(acrToken),
- }, nil
- }
- type accessTokenFetcher func(acrRefreshToken, tenantID, registryURL, scope string) (string, error)
- func fetchACRAccessToken(acrRefreshToken, _, registryURL, scope string) (string, error) {
- formData := url.Values{
- "grant_type": {"refresh_token"},
- "service": {registryURL},
- "scope": {scope},
- "refresh_token": {acrRefreshToken},
- }
- res, err := http.PostForm(fmt.Sprintf("https://%s/oauth2/token", registryURL), formData)
- if err != nil {
- return "", err
- }
- defer res.Body.Close()
- if res.StatusCode != http.StatusOK {
- return "", fmt.Errorf("could not generate access token, unexpected status code: %d", res.StatusCode)
- }
- body, err := io.ReadAll(res.Body)
- if err != nil {
- return "", err
- }
- var payload map[string]string
- err = json.Unmarshal(body, &payload)
- if err != nil {
- return "", err
- }
- accessToken, ok := payload["access_token"]
- if !ok {
- return "", fmt.Errorf("unable to get token")
- }
- return accessToken, nil
- }
- type refreshTokenFetcher func(aadAccessToken, tenantID, registryURL string) (string, error)
- func fetchACRRefreshToken(aadAccessToken, tenantID, registryURL string) (string, error) {
- // https://github.com/Azure/acr/blob/main/docs/AAD-OAuth.md#overview
- // https://docs.microsoft.com/en-us/azure/container-registry/container-registry-authentication?tabs=azure-cli
- formData := url.Values{
- "grant_type": {"access_token"},
- "service": {registryURL},
- "tenant": {tenantID},
- "access_token": {aadAccessToken},
- }
- res, err := http.PostForm(fmt.Sprintf("https://%s/oauth2/exchange", registryURL), formData)
- if err != nil {
- return "", err
- }
- defer res.Body.Close()
- if res.StatusCode != http.StatusOK {
- return "", fmt.Errorf("count not generate refresh token, unexpected status code %d, expected %d", res.StatusCode, http.StatusOK)
- }
- body, err := io.ReadAll(res.Body)
- if err != nil {
- return "", err
- }
- var payload map[string]string
- err = json.Unmarshal(body, &payload)
- if err != nil {
- return "", err
- }
- refreshToken, ok := payload["refresh_token"]
- if !ok {
- return "", fmt.Errorf("unable to get token")
- }
- return refreshToken, nil
- }
- func accessTokenForWorkloadIdentity(ctx context.Context, crClient client.Client, kubeClient kcorev1.CoreV1Interface, envType v1beta1.AzureEnvironmentType, serviceAccountRef *smmeta.ServiceAccountSelector, namespace string) (string, error) {
- aadEndpoint := keyvault.AadEndpointForType(envType)
- scope := keyvault.ServiceManagementEndpointForType(envType)
- // if no serviceAccountRef was provided
- // we expect certain env vars to be present.
- // They are set by the azure workload identity webhook.
- if serviceAccountRef == nil {
- clientID := os.Getenv("AZURE_CLIENT_ID")
- tenantID := os.Getenv("AZURE_TENANT_ID")
- tokenFilePath := os.Getenv("AZURE_FEDERATED_TOKEN_FILE")
- if clientID == "" || tenantID == "" || tokenFilePath == "" {
- return "", errors.New("missing environment variables")
- }
- token, err := os.ReadFile(tokenFilePath)
- if err != nil {
- return "", fmt.Errorf("unable to read token file %s: %w", tokenFilePath, err)
- }
- tp, err := keyvault.NewTokenProvider(ctx, string(token), clientID, tenantID, aadEndpoint, scope)
- if err != nil {
- return "", err
- }
- return tp.OAuthToken(), nil
- }
- var sa corev1.ServiceAccount
- err := crClient.Get(ctx, types.NamespacedName{
- Name: serviceAccountRef.Name,
- Namespace: namespace,
- }, &sa)
- if err != nil {
- return "", err
- }
- clientID, ok := sa.ObjectMeta.Annotations[keyvault.AnnotationClientID]
- if !ok {
- return "", fmt.Errorf("service account is missing annoation: %s", keyvault.AnnotationClientID)
- }
- tenantID, ok := sa.ObjectMeta.Annotations[keyvault.AnnotationTenantID]
- if !ok {
- return "", fmt.Errorf("service account is missing annotation: %s", keyvault.AnnotationTenantID)
- }
- audiences := []string{keyvault.AzureDefaultAudience}
- if len(serviceAccountRef.Audiences) > 0 {
- audiences = append(audiences, serviceAccountRef.Audiences...)
- }
- token, err := keyvault.FetchSAToken(ctx, namespace, serviceAccountRef.Name, audiences, kubeClient)
- if err != nil {
- return "", err
- }
- tp, err := keyvault.NewTokenProvider(ctx, token, clientID, tenantID, aadEndpoint, scope)
- if err != nil {
- return "", err
- }
- return tp.OAuthToken(), nil
- }
- func accessTokenForManagedIdentity(ctx context.Context, envType v1beta1.AzureEnvironmentType, identityID string) (string, error) {
- // handle workload identity
- creds, err := azidentity.NewManagedIdentityCredential(
- &azidentity.ManagedIdentityCredentialOptions{
- ID: azidentity.ResourceID(identityID),
- },
- )
- if err != nil {
- return "", err
- }
- aud := audienceForType(envType)
- accessToken, err := creds.GetToken(ctx, policy.TokenRequestOptions{
- Scopes: []string{aud},
- })
- if err != nil {
- return "", err
- }
- return accessToken.Token, nil
- }
- func (g *Generator) accessTokenForServicePrincipal(ctx context.Context, crClient client.Client, namespace string, envType v1beta1.AzureEnvironmentType, tenantID string, idRef, secretRef smmeta.SecretKeySelector) (string, error) {
- cid, err := secretKeyRef(ctx, crClient, namespace, idRef)
- if err != nil {
- return "", err
- }
- csec, err := secretKeyRef(ctx, crClient, namespace, secretRef)
- if err != nil {
- return "", err
- }
- aadEndpoint := keyvault.AadEndpointForType(envType)
- p := azidentity.ClientSecretCredentialOptions{}
- p.Cloud.ActiveDirectoryAuthorityHost = aadEndpoint
- creds, err := g.clientSecretCreds(
- tenantID,
- cid,
- csec,
- &p)
- if err != nil {
- return "", err
- }
- aud := audienceForType(envType)
- accessToken, err := creds.GetToken(ctx, policy.TokenRequestOptions{
- Scopes: []string{aud},
- })
- if err != nil {
- return "", err
- }
- return accessToken.Token, nil
- }
- // secretKeyRef fetches a secret key.
- func secretKeyRef(ctx context.Context, crClient client.Client, namespace string, secretRef smmeta.SecretKeySelector) (string, error) {
- var secret corev1.Secret
- ref := types.NamespacedName{
- Namespace: namespace,
- Name: secretRef.Name,
- }
- err := crClient.Get(ctx, ref, &secret)
- if err != nil {
- return "", fmt.Errorf("unable to find namespace=%q secret=%q %w", ref.Namespace, ref.Name, err)
- }
- keyBytes, ok := secret.Data[secretRef.Key]
- if !ok {
- return "", fmt.Errorf("unable to find key=%q secret=%q namespace=%q", secretRef.Key, secretRef.Name, namespace)
- }
- value := strings.TrimSpace(string(keyBytes))
- return value, nil
- }
- func audienceForType(t v1beta1.AzureEnvironmentType) string {
- suffix := ".default"
- switch t {
- case v1beta1.AzureEnvironmentChinaCloud:
- return azure.ChinaCloud.TokenAudience + suffix
- case v1beta1.AzureEnvironmentGermanCloud:
- return azure.GermanCloud.TokenAudience + suffix
- case v1beta1.AzureEnvironmentUSGovernmentCloud:
- return azure.USGovernmentCloud.TokenAudience + suffix
- case v1beta1.AzureEnvironmentPublicCloud, "":
- return azure.PublicCloud.TokenAudience + suffix
- }
- return azure.PublicCloud.TokenAudience + suffix
- }
- func parseSpec(data []byte) (*genv1alpha1.ACRAccessToken, error) {
- var spec genv1alpha1.ACRAccessToken
- err := yaml.Unmarshal(data, &spec)
- return &spec, err
- }
- func init() {
- genv1alpha1.Register(genv1alpha1.ACRAccessTokenKind, &Generator{})
- }
|