Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add support for azure workload identity in Microsoft Entra SSO. #21433

Merged
merged 13 commits into from
Jan 29, 2025
Merged
Show file tree
Hide file tree
Changes from 6 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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ node_modules/
.envrc.remote
.*.swp
rerunreport.txt
token.txt

# ignore built binaries
cmd/argocd/argocd
Expand Down
153 changes: 83 additions & 70 deletions docs/operator-manual/user-management/microsoft.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,76 +3,10 @@
!!! note ""
Entra ID was formerly known as Azure AD.

* [Entra ID SAML Enterprise App Auth using Dex](#entra-id-saml-enterprise-app-auth-using-dex)
* [Entra ID App Registration Auth using OIDC](#entra-id-app-registration-auth-using-oidc)
* [Entra ID SAML Enterprise App Auth using Dex](#entra-id-saml-enterprise-app-auth-using-dex)
* [Entra ID App Registration Auth using Dex](#entra-id-app-registration-auth-using-dex)

## Entra ID SAML Enterprise App Auth using Dex
### Configure a new Entra ID Enterprise App

1. From the `Microsoft Entra ID` > `Enterprise applications` menu, choose `+ New application`
2. Select `Non-gallery application`
3. Enter a `Name` for the application (e.g. `Argo CD`), then choose `Add`
4. Once the application is created, open it from the `Enterprise applications` menu.
5. From the `Users and groups` menu of the app, add any users or groups requiring access to the service.
![Azure Enterprise SAML Users](../../assets/azure-enterprise-users.png "Azure Enterprise SAML Users")
6. From the `Single sign-on` menu, edit the `Basic SAML Configuration` section as follows (replacing `my-argo-cd-url` with your Argo URL):
- **Identifier (Entity ID):** https://`<my-argo-cd-url>`/api/dex/callback
- **Reply URL (Assertion Consumer Service URL):** https://`<my-argo-cd-url>`/api/dex/callback
- **Sign on URL:** https://`<my-argo-cd-url>`/auth/login
- **Relay State:** `<empty>`
- **Logout Url:** `<empty>`
![Azure Enterprise SAML URLs](../../assets/azure-enterprise-saml-urls.png "Azure Enterprise SAML URLs")
7. From the `Single sign-on` menu, edit the `User Attributes & Claims` section to create the following claims:
- `+ Add new claim` | **Name:** email | **Source:** Attribute | **Source attribute:** user.mail
- `+ Add group claim` | **Which groups:** All groups | **Source attribute:** Group ID | **Customize:** True | **Name:** Group | **Namespace:** `<empty>` | **Emit groups as role claims:** False
- *Note: The `Unique User Identifier` required claim can be left as the default `user.userprincipalname`*
![Azure Enterprise SAML Claims](../../assets/azure-enterprise-claims.png "Azure Enterprise SAML Claims")
8. From the `Single sign-on` menu, download the SAML Signing Certificate (Base64)
- Base64 encode the contents of the downloaded certificate file, for example:
- `$ cat ArgoCD.cer | base64`
- *Keep a copy of the encoded output to be used in the next section.*
9. From the `Single sign-on` menu, copy the `Login URL` parameter, to be used in the next section.

### Configure Argo to use the new Entra ID Enterprise App

1. Edit `argocd-cm` and add the following `dex.config` to the data section, replacing the `caData`, `my-argo-cd-url` and `my-login-url` your values from the Entra ID App:

data:
url: https://my-argo-cd-url
dex.config: |
logger:
level: debug
format: json
connectors:
- type: saml
id: saml
name: saml
config:
entityIssuer: https://my-argo-cd-url/api/dex/callback
ssoURL: https://my-login-url (e.g. https://login.microsoftonline.com/xxxxx/a/saml2)
caData: |
MY-BASE64-ENCODED-CERTIFICATE-DATA
redirectURI: https://my-argo-cd-url/api/dex/callback
usernameAttr: email
emailAttr: email
groupsAttr: Group

2. Edit `argocd-rbac-cm` to configure permissions, similar to example below.
- Use Entra ID `Group IDs` for assigning roles.
- See [RBAC Configurations](../rbac.md) for more detailed scenarios.

# example policy
policy.default: role:readonly
policy.csv: |
p, role:org-admin, applications, *, */*, allow
p, role:org-admin, clusters, get, *, allow
p, role:org-admin, repositories, get, *, allow
p, role:org-admin, repositories, create, *, allow
p, role:org-admin, repositories, update, *, allow
p, role:org-admin, repositories, delete, *, allow
g, "84ce98d1-e359-4f3b-85af-985b458de3c6", role:org-admin # (azure group assigned to role)

## Entra ID App Registration Auth using OIDC
### Configure a new Entra ID App registration
#### Add a new Entra ID App registration
Expand All @@ -96,7 +30,18 @@
![Azure App registration's Authentication](../../assets/azure-app-registration-authentication.png "Azure App registration's Authentication")

#### Add credentials a new Entra ID App registration

##### Using Workload Identity Federation (Recommended)
1. **Label the Pods:** Add the `azure.workload.identity/use: "true"` label to the `argocd-server` pods.
2. **Add Annotation to Service Account:** Add `azure.workload.identity/client-id: "$CLIENT_ID"` annotation to the `argocd-server` service account using the details from application created in previous step.
3. From the `Certificates & secrets` menu, navigate to `Federated credentials`, then choose `+ Add credential`
4. Choose `Federated credential scenario` as `Kubernetes Accessing Azure resources`
- Enter Cluster Issuer URL, refer to [retrieve the OIDC issuer URL](https://learn.microsoft.com/en-us/azure/aks/workload-identity-deploy-cluster#retrieve-the-oidc-issuer-url) documentation
- Enter namespace as the namespace where the argocd is deployed
- Enter service account name as `argocd-server`
- Enter a unique name
- Click Add.

##### Using Client Secret
1. From the `Certificates & secrets` menu, choose `+ New client secret`
2. Enter a `Name` for the secret (e.g. `ArgoCD-SSO`).
- Make sure to copy and save generated value. This is a value for the `client_secret`.
Expand Down Expand Up @@ -129,7 +74,9 @@
name: Azure
issuer: https://login.microsoftonline.com/{directory_tenant_id}/v2.0
clientID: {azure_ad_application_client_id}
clientSecret: $oidc.azure.clientSecret
clientSecret: $oidc.azure.clientSecret // if using client secret for authentication
azure:
useWorkloadIdentity: true // if using azure workload identity for authentication
requestedIDTokenClaims:
groups:
essential: true
Expand All @@ -139,7 +86,7 @@
- profile
- email

2. Edit `argocd-secret` and configure the `data.oidc.azure.clientSecret` section:
2. Skip this step if using azure workload identity. Edit `argocd-secret` and configure the `data.oidc.azure.clientSecret` section:

Secret -> argocd-secret

Expand Down Expand Up @@ -177,6 +124,72 @@

Refer to [operator-manual/argocd-rbac-cm.yaml](https://github.com/argoproj/argo-cd/blob/master/docs/operator-manual/argocd-rbac-cm.yaml) for all of the available variables.

## Entra ID SAML Enterprise App Auth using Dex
### Configure a new Entra ID Enterprise App

1. From the `Microsoft Entra ID` > `Enterprise applications` menu, choose `+ New application`
2. Select `Non-gallery application`
3. Enter a `Name` for the application (e.g. `Argo CD`), then choose `Add`
4. Once the application is created, open it from the `Enterprise applications` menu.
5. From the `Users and groups` menu of the app, add any users or groups requiring access to the service.
![Azure Enterprise SAML Users](../../assets/azure-enterprise-users.png "Azure Enterprise SAML Users")
6. From the `Single sign-on` menu, edit the `Basic SAML Configuration` section as follows (replacing `my-argo-cd-url` with your Argo URL):
- **Identifier (Entity ID):** https://`<my-argo-cd-url>`/api/dex/callback
- **Reply URL (Assertion Consumer Service URL):** https://`<my-argo-cd-url>`/api/dex/callback
- **Sign on URL:** https://`<my-argo-cd-url>`/auth/login
- **Relay State:** `<empty>`
- **Logout Url:** `<empty>`
![Azure Enterprise SAML URLs](../../assets/azure-enterprise-saml-urls.png "Azure Enterprise SAML URLs")
7. From the `Single sign-on` menu, edit the `User Attributes & Claims` section to create the following claims:
- `+ Add new claim` | **Name:** email | **Source:** Attribute | **Source attribute:** user.mail
- `+ Add group claim` | **Which groups:** All groups | **Source attribute:** Group ID | **Customize:** True | **Name:** Group | **Namespace:** `<empty>` | **Emit groups as role claims:** False
- *Note: The `Unique User Identifier` required claim can be left as the default `user.userprincipalname`*
![Azure Enterprise SAML Claims](../../assets/azure-enterprise-claims.png "Azure Enterprise SAML Claims")
8. From the `Single sign-on` menu, download the SAML Signing Certificate (Base64)
- Base64 encode the contents of the downloaded certificate file, for example:
- `$ cat ArgoCD.cer | base64`
- *Keep a copy of the encoded output to be used in the next section.*
9. From the `Single sign-on` menu, copy the `Login URL` parameter, to be used in the next section.

### Configure Argo to use the new Entra ID Enterprise App

1. Edit `argocd-cm` and add the following `dex.config` to the data section, replacing the `caData`, `my-argo-cd-url` and `my-login-url` your values from the Entra ID App:

data:
url: https://my-argo-cd-url
dex.config: |
logger:
level: debug
format: json
connectors:
- type: saml
id: saml
name: saml
config:
entityIssuer: https://my-argo-cd-url/api/dex/callback
ssoURL: https://my-login-url (e.g. https://login.microsoftonline.com/xxxxx/a/saml2)
caData: |
MY-BASE64-ENCODED-CERTIFICATE-DATA
redirectURI: https://my-argo-cd-url/api/dex/callback
usernameAttr: email
emailAttr: email
groupsAttr: Group

2. Edit `argocd-rbac-cm` to configure permissions, similar to example below.
- Use Entra ID `Group IDs` for assigning roles.
- See [RBAC Configurations](../rbac.md) for more detailed scenarios.

# example policy
policy.default: role:readonly
policy.csv: |
p, role:org-admin, applications, *, */*, allow
p, role:org-admin, clusters, get, *, allow
p, role:org-admin, repositories, get, *, allow
p, role:org-admin, repositories, create, *, allow
p, role:org-admin, repositories, update, *, allow
p, role:org-admin, repositories, delete, *, allow
g, "84ce98d1-e359-4f3b-85af-985b458de3c6", role:org-admin # (azure group assigned to role)

## Entra ID App Registration Auth using Dex

Configure a new AD App Registration, as above.
Expand Down
81 changes: 72 additions & 9 deletions util/oidc/oidc.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package oidc

import (
"context"
"encoding/hex"
"encoding/json"
"errors"
Expand All @@ -14,6 +15,7 @@ import (
"os"
"path"
"strings"
"sync"
"time"

gooidc "github.com/coreos/go-oidc/v3/oidc"
Expand Down Expand Up @@ -60,6 +62,8 @@ type ClientApp struct {
clientID string
// OAuth2 client secret of this application
clientSecret string
// Use Azure Workload Identity for clientID auth instead of clientSecret
useAzureWorkloadIdentity bool
// Callback URL for OAuth2 responses (e.g. https://argocd.example.com/auth/callback)
redirectURI string
// URL of the issuer (e.g. https://argocd.example.com/api/dex)
Expand All @@ -81,6 +85,10 @@ type ClientApp struct {
provider Provider
// clientCache represent a cache of sso artifact
clientCache cache.CacheClient
// properties for azure workload identity.
assertion string
expires time.Time
mtx *sync.RWMutex
jagpreetstamber marked this conversation as resolved.
Show resolved Hide resolved
}

func GetScopesOrDefault(scopes []string) []string {
Expand All @@ -102,14 +110,16 @@ func NewClientApp(settings *settings.ArgoCDSettings, dexServerAddr string, dexTl
return nil, err
}
a := ClientApp{
clientID: settings.OAuth2ClientID(),
clientSecret: settings.OAuth2ClientSecret(),
redirectURI: redirectURL,
issuerURL: settings.IssuerURL(),
userInfoPath: settings.UserInfoPath(),
baseHRef: baseHRef,
encryptionKey: encryptionKey,
clientCache: cacheClient,
clientID: settings.OAuth2ClientID(),
clientSecret: settings.OAuth2ClientSecret(),
useAzureWorkloadIdentity: settings.UseAzureWorkloadIdentity(),
redirectURI: redirectURL,
issuerURL: settings.IssuerURL(),
userInfoPath: settings.UserInfoPath(),
baseHRef: baseHRef,
encryptionKey: encryptionKey,
clientCache: cacheClient,
mtx: &sync.RWMutex{},
}
log.Infof("Creating client app (%s)", a.clientID)
u, err := url.Parse(settings.URL)
Expand Down Expand Up @@ -158,6 +168,7 @@ func (a *ClientApp) oauth2Config(request *http.Request, scopes []string) (*oauth
log.Warnf("Unable to find ArgoCD URL from request, falling back to configured redirect URI: %v", err)
redirectURL = a.redirectURI
}

return &oauth2.Config{
ClientID: a.clientID,
ClientSecret: a.clientSecret,
Expand Down Expand Up @@ -336,6 +347,41 @@ func (a *ClientApp) HandleLogin(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, url, http.StatusSeeOther)
}

// getAzureKubernetesFederatedServiceAccountToken returns the specified file's content, which is expected to be Federated Kubernetes service account token.
// Kubernetes is responsible for updating the file as service account tokens expire.
func (a *ClientApp) getAzureKubernetesFederatedServiceAccountToken(context.Context) (string, error) {
file := ""
ok := false
if file == "" {
if file, ok = os.LookupEnv("AZURE_FEDERATED_TOKEN_FILE"); !ok {
jagpreetstamber marked this conversation as resolved.
Show resolved Hide resolved
return "", errors.New("no token file specified. Check pod configuration or set TokenFilePath in the options")
}
}

a.mtx.RLock()
if a.expires.Before(time.Now()) {
// ensure only one goroutine at a time updates the assertion
a.mtx.RUnlock()
a.mtx.Lock()
defer a.mtx.Unlock()
// double check because another goroutine may have acquired the write lock first and done the update
if now := time.Now(); a.expires.Before(now) {
content, err := os.ReadFile(file)
if err != nil {
return "", err
}
a.assertion = string(content)
// Kubernetes rotates service account tokens when they reach 80% of their total TTL. The shortest TTL
// is 1 hour. That implies the token we just read is valid for at least 12 minutes (20% of 1 hour),
// but we add some margin for safety.
a.expires = now.Add(10 * time.Minute)
jagpreetstamber marked this conversation as resolved.
Show resolved Hide resolved
}
} else {
defer a.mtx.RUnlock()
}
return a.assertion, nil
}

// HandleCallback is the callback handler for an OAuth2 login flow
func (a *ClientApp) HandleCallback(w http.ResponseWriter, r *http.Request) {
oauth2Config, err := a.oauth2Config(r, nil)
Expand All @@ -361,12 +407,29 @@ func (a *ClientApp) HandleCallback(w http.ResponseWriter, r *http.Request) {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}

ctx := gooidc.ClientContext(r.Context(), a.client)
token, err := oauth2Config.Exchange(ctx, code)
options := []oauth2.AuthCodeOption{}

if a.useAzureWorkloadIdentity {
clientAssertion, err := a.getAzureKubernetesFederatedServiceAccountToken(ctx)
if err != nil {
http.Error(w, fmt.Sprintf("failed to generate client assertion: %v", err), http.StatusInternalServerError)
return
}

options = []oauth2.AuthCodeOption{
oauth2.SetAuthURLParam("client_assertion_type", "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"),
oauth2.SetAuthURLParam("client_assertion", clientAssertion),
}
}

token, err := oauth2Config.Exchange(ctx, code, options...)
if err != nil {
http.Error(w, fmt.Sprintf("failed to get token: %v", err), http.StatusInternalServerError)
return
}

idTokenRAW, ok := token.Extra("id_token").(string)
if !ok {
http.Error(w, "no id_token in token response", http.StatusInternalServerError)
Expand Down
Loading
Loading