Skip to content

Commit

Permalink
feat: Add support for azure workload identity in Microsoft Entra SSO.
Browse files Browse the repository at this point in the history
Signed-off-by: Jagpreet Singh Tamber <[email protected]>

use client assertion in the idtoken generation.

Signed-off-by: Jagpreet Singh Tamber <[email protected]>

Use Kubernetes Service Account Token for assertion.

Signed-off-by: Jagpreet Singh Tamber <[email protected]>
  • Loading branch information
jagpreetstamber committed Jan 10, 2025
1 parent 1645d57 commit fdc0498
Show file tree
Hide file tree
Showing 9 changed files with 331 additions and 93 deletions.
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
154 changes: 83 additions & 71 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,8 +30,19 @@
![Azure App registration's Authentication](../../assets/azure-app-registration-authentication.png "Azure App registration's Authentication")

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

1. From the `Certificates & secrets` menu, choose `+ New client secret`
##### 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, choose `+ New client secret`
4. Choose `Federated credential scenario` as `Kubernetes Accessing Azure resources`
- Enter Cluster Issuer URL, refer [Retreive the OIDC issuer URL](https://learn.microsoft.com/en-us/azure/aks/workload-identity-deploy-cluster#retrieve-the-oidc-issuer-url)
- 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, Navigate to `Federated credentials` choose `+ Add credential`
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`.
![Azure App registration's Secret](../../assets/azure-app-registration-secret.png "Azure App registration's Secret")
Expand Down Expand Up @@ -129,7 +74,8 @@
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
useAzureWorkloadIdentity: true // if using azure workload identity for authentication
requestedIDTokenClaims:
groups:
essential: true
Expand All @@ -139,7 +85,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 +123,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
2 changes: 1 addition & 1 deletion server/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -624,7 +624,7 @@ func getTestServer(t *testing.T, anonymousEnabled bool, withFakeSSO bool, useDex
})
oidcServer := ts
if !useDexForSSO {
oidcServer = testutil.GetOIDCTestServer(t)
oidcServer = testutil.GetOIDCTestServer(t, false)
}
if withFakeSSO {
cm.Data["url"] = ts.URL
Expand Down
80 changes: 71 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,9 @@ type ClientApp struct {
provider Provider
// clientCache represent a cache of sso artifact
clientCache cache.CacheClient
assertion string
expires time.Time
mtx *sync.RWMutex
}

func GetScopesOrDefault(scopes []string) []string {
Expand All @@ -102,14 +109,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 +167,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 +346,41 @@ func (a *ClientApp) HandleLogin(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, url, http.StatusSeeOther)
}

// getAzureKubernetesServiceAccountToken returns the specified file's content, which is expected to be a Kubernetes service account token.
// Kubernetes is responsible for updating the file as service account tokens expire.
func (a *ClientApp) getAzureKubernetesServiceAccountToken(context.Context) (string, error) {
file := ""
ok := false
if file == "" {
if file, ok = os.LookupEnv("AZURE_FEDERATED_TOKEN_FILE"); !ok {
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)
}
} 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 +406,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.getAzureKubernetesServiceAccountToken(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

0 comments on commit fdc0498

Please sign in to comment.