Integrar Argo CD con Azure Active Directory

Como sabes, una de las ventajas de usar Argo CD frente a otros operadores para GitOps es que este tiene una gestión visual de todo lo que se despliega en tus clústeres a través de él. Por defecto, este viene configurado con un único usuario admin, pero es posible integrarlo con diferentes proveedores de identidad y delegar en estos el inicio de sesión. Es por ello que es posible usar Azure Active Directory para este cometido y mapear tus grupos de seguridad a los permisos que consideres oportuno de Argo CD. En este artículo te cuento cómo hacerlo.

Crear un cluster de AKS con Argo CD

Si todavía no tienes uno, puedes crearte un clúster de AKS con Argo CD con estos comandos:

# Variables
RESOURCE_GROUP="ArgoCD-Demo"
AKS_NAME="aks-argocd-demo"
LOCATION="North Europe"
# Create a resource group
az group create -n $RESOURCE_GROUP -l $LOCATION
# Create an AKS cluster
az aks create \
-n $AKS_NAME \
-g $RESOURCE_GROUP \
--node-count 1 \
--generate-ssh-keys
# Get AKS credentials
az aks get-credentials -n $AKS_NAME -g $RESOURCE_GROUP
# Deploy ArgoCD
kubectl create namespace argocd
kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml

Para comprobar que todo está funcionando correctamente puedes acceder a la interfaz a través de port forwarding:

# Get the password (user: admin)
kubectl -n argocd get secret argocd-initial-admin-secret -o jsonpath="{.data.password}" | base64 -d
# Access ArgoCD UI
kubectl port-forward svc/argocd-server -n argocd 8080:443

Configurar aplicación en Azure Active Directory

Siempre que quieres integrar un sistema o aplicación con Azure Active Directory necesitas registrar una aplicación en este:

# Login in the right tenant
az login
# Register Azure AD application
APP_NAME="argocd"
az ad app create \
--display-name $APP_NAME \
--reply-urls https://localhost:8080/auth/callback \
--required-resource-accesses @required-resource-accesses.json \
--optional-claims @optional-claims.json 

Si te fijas, al crear esta aplicación estoy utilizando dos archivos externos para configurar las secciones API Permissions (required-resource-accesses.json) y Token Configuration (optional-claims.json). El primero de ellos tiene esta pinta:

[{
    "resourceAppId": "00000003-0000-0000-c000-000000000000",
    "resourceAccess": [{
        "id": "e1fe6dd8-ba31-4d61-89e7-88639da4683d",
        "type": "Scope"
    }]
}]

¿De dónde se saca esta información? En este caso lo que se pretende es recuperar el scope de Microsoft Graph llamado User.Read. Estos datos he sido capaz de conseguirlos usando estos comandos:

# resource id
az ad sp list --query "[?appDisplayName=='Microsoft Graph'].{permissions:oauth2Permissions}[0].permissions[?value=='User.Read'].id" --all | jq -r '.[]'
# resourceAppId
az ad sp list --query "[?appDisplayName=='Microsoft Graph'].appId" --all | jq -r '.[]' 

El resultado en la interfaz será el siguiente:

API Permissions para Argo CD en la interfaz del portal de Azure
API Permissions para Argo CD en la interfaz del portal de Azure

Por otro lado, el archivo optional-claims.json lo que hace es configurar la claims groups para todos los tokens:

{
    "idToken": [{
        "additionalProperties": [],
        "essential": false,
        "name": "groups",
        "source": null
    }],
    "accessToken": [{
        "additionalProperties": [],
        "essential": false,
        "name": "groups",
        "source": null
    }],
    "saml2Token": [{
        "additionalProperties": [],
        "essential": false,
        "name": "groups",
        "source": null
    }]
}

Modificando de esta manera el apartado Token Configuration:

Token configuration para que Argo CD obtenga los grupos de Azure AD
Token configuration para que Argo CD obtenga los grupos de Azure AD

Para que esto último tenga el efecto que esperamos, necesitamos además actualizar el valor groupMembershipClaims del manifiesto de la aplicación:

# Get app id
APP_ID=$(az ad app list --query "[?displayName=='$APP_NAME'].appId" -o tsv)
OBJECT_ID=$(az ad app list --query "[?displayName=='$APP_NAME'].objectId" -o tsv)
# Update app manifest
az rest --method patch \
--uri "https://graph.microsoft.com/v1.0/applications/$OBJECT_ID" \
--body '{    
        "groupMembershipClaims": "ApplicationGroup",
        "web": {
          "implicitGrantSettings": {
              "enableIdTokenIssuance": false
          }
    }    
}' \
--verbose

Este debe tener el valor ApplicationGroup. Por otro lado, he aprovechado la llamada para deshabilitar el flujo Implicit Grant el emitir el id token, ya que no es necesario.

Para finalizar, necesitamos crear un service principal de la aplicación registrada para posteriormente poder asignarle los grupos de nuestro Azure AD que queremos que interactúen con nuestra aplicación:

# Create a service principal for the app
az ad sp create --id $APP_ID
SP_OBJECT_ID=$(az ad sp list --query "[?displayName=='$APP_NAME'].objectId" --all -o tsv)
az rest \
--method patch \
--uri "https://graph.microsoft.com/v1.0/servicePrincipals/$SP_OBJECT_ID" \
--body '{ "tags": ["WindowsAzureActiveDirectoryIntegratedApp" ] }' \
--verbose

Crear grupos de usuario de Azure Active Directory

Ahora que ya tienes la aplicación correctamente configurada lo siguiente que necesitas hacer es crear un par de grupos de seguridad de Azure AD para esta prueba, además de asignarle algunos usuarios para probar el entorno:

# Create a group in Azure AD for ArgoCD
ADMIN_GROUP_NAME="argocd-admin"
az ad group create \
--display-name $ADMIN_GROUP_NAME \
--mail-nickname $ADMIN_GROUP_NAME 
# List users in Azure AD
az ad user list \
--query "[].{displayName:displayName,objectId:objectId}" \
-o table
BRAN_STARK=$(az ad user list --query "[?displayName=='Bran Stark'].objectId" -o tsv)
# Add some users to the group
az ad group member add \
--group $ADMIN_GROUP_NAME \
--member-id $BRAN_STARK
READER_GROUP_NAME="argocd-readers"
az ad group create \
--display-name $READER_GROUP_NAME \
--mail-nickname $READER_GROUP_NAME 
JAIME_LANNISTER=$(az ad user list --query "[?displayName=='Jaime Lannister'].objectId" -o tsv)
# Add some users to the group
az ad group member add \
--group $READER_GROUP_NAME \
--member-id $JAIME_LANNISTER

Si lo prefieres puedes saltarte este paso y usar grupos de usuarios existentes.

Por último, es necesario asignar estos grupos a la enterprise application generada, que se creó al crear el service principal:

# Assign groups to the enterprise application
ADMIN_GROUP_ID=$(az ad group show --group "$ADMIN_GROUP_NAME" --query objectId --out tsv)
# Assign the group to the app registration
az rest --method post \
--uri "https://graph.microsoft.com/v1.0/groups/$ADMIN_GROUP_ID/appRoleAssignments" \
--body "{
  \"appRoleId\": \"00000000-0000-0000-0000-000000000000\",
  \"principalId\": \"$ADMIN_GROUP_ID\",
  \"resourceId\": \"$SP_OBJECT_ID\"  
}"
READER_GROUP_ID=$(az ad group show --group "$READER_GROUP_NAME" --query objectId --out tsv)
# Assign the group to the app registration
az rest --method post \
--uri "https://graph.microsoft.com/v1.0/groups/$READER_GROUP_ID/appRoleAssignments" \
--body "{
  \"appRoleId\": \"00000000-0000-0000-0000-000000000000\",
  \"principalId\": \"$READER_GROUP_ID\",
  \"resourceId\": \"$SP_OBJECT_ID\"  
}"

Configurar Argo CD para la integración

Desde el punto de vista de Azure AD ya tienes todo lo que necesitas configurado. Ahora falta que Argo CD sea consciente de todo esto. Para ello debes modificar varios archivos de su configuración.

En primer lugar vamos a configurar el secreto argocd-secret que es donde debemos guardar, en base64, el valor del client secret para nuestra aplicación:

# Edit argocd-secret with the value of the client secret in base 64
PASSWORD=$(az ad app credential reset \
--id $(az ad app list --query "[?displayName=='$APP_NAME'].appId" --all -o tsv) \
--query "password" -o tsv)
echo $PASSWORD | base64

Para que la edición de los manifiestos sea lo más fácil posible utilizo Visual Studio Code añadiendo esta variable en la sesión actual:

# Use VS code to edit the files
export KUBE_EDITOR="code --wait"

y ahora ya modifico el secreto:

kubectl edit secret argocd-secret -n argocd

Siguiendo el ejemplo de la documentación oficial, el valor de echo $PASSWORD | base64 lo asigno a una clave llamada oidc.azure.clientSecret. El resultado del manifiesto será algo parecido a esto:

# Please edit the object below. Lines beginning with a '#' will be ignored,
# and an empty file will abort the edit. If an error occurs while saving this file will be
# reopened with the relevant failures.
#
apiVersion: v1
data:
  admin.password: <ADMIN_PASSWORD>
  admin.passwordMtime: <ADMIN_PASSWORDMTIME>
  oidc.azure.clientSecret: <YOUR_APPLICATION_CLIENT_SECRET_IN_BASE64>
  server.secretkey: 
  tls.crt: <TLS_CERT>
  tls.key:<TLS_KEY>
kind: Secret
metadata:
  annotations:
    kubectl.kubernetes.io/last-applied-configuration: |
      {"apiVersion":"v1","kind":"Secret","metadata":{"annotations":{},"labels":{"app.kubernetes.io/name":"argocd-secret","app.kubernetes.io/part-of":"argocd"},"name":"argocd-secret","namespace":"argocd"},"type":"Opaque"}
[...]

El siguiente archivo a modificar es el llamado argocd-cm, donde Argo CD configura el proveedor de identidad:

# Edit argocd-cm config map to add the Azure AD client id and tenant id
kubectl edit cm argocd-cm -n argocd

En él debe quedar la configuración como lo siguiente:

# Please edit the object below. Lines beginning with a '#' will be ignored,
# and an empty file will abort the edit. If an error occurs while saving this file will be
# reopened with the relevant failures.
#
apiVersion: v1
data:
  oidc.config: |
    name: Azure
    issuer: https://login.microsoftonline.com/<YOUR_TENANT_ID>/v2.0
    clientID: <YOUR_APP_CLIENT_ID>
    clientSecret: $oidc.azure.clientSecret
    requestedIDTokenClaims:
        groups:
            essential: true
    requestedScopes:
        - openid
        - profile
        - email
  url: https://localhost:8080
kind: ConfigMap
metadata:
  annotations:
    kubectl.kubernetes.io/last-applied-configuration: |
      {"apiVersion":"v1","kind":"ConfigMap","metadata":{"annotations":{},"labels":{"app.kubernetes.io/name":"argocd-cm","app.kubernetes.io/part-of":"argocd"},"name":"argocd-cm","namespace":"argocd"}}
 [...]

Esta información puedes recuperarla tanto en el apartado Overview de la aplicación registrada en Azure AD o bien a través de Azure CLI:

# App ID
az ad app list --query "[?displayName=='$APP_NAME'].appId" -o tsv
# Azure tenant ID
az ad sp show --id $APP_ID --query "appOwnerTenantId" -o tsv

Y ya por finalizar con las configuraciones, el último paso es generar los roles, y sus permisos, que queremos utilizar con los usuarios que pertenezcan a los grupos que hemos utilizado como parte de nuestra configuración. Para ello tenemos que modificar el config map argocd-rbac-cm:

# Edit argocd-rbac-cm to assign the groups to the roles
kubectl edit cm argocd-rbac-cm -n argocd

Para este ejemplo he creado una configuración como la siguiente:

# Please edit the object below. Lines beginning with a '#' will be ignored,
# and an empty file will abort the edit. If an error occurs while saving this file will be
# reopened with the relevant failures.
#
apiVersion: v1
data:
  policy.csv: |
    p, role:org-admin, applications, *, */*, allow
    p, role:org-admin, projects, *, *, allow
    p, role:org-admin, clusters, *, *, 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, bd045cd8-cfc0-44d9-a46b-80043ae6868a, role:org-admin
    g, 40c7463f-b5cf-430d-842e-ed8abdb9d840, role:readonly
  policy.default: role:readonly
kind: ConfigMap
metadata:
  annotations:
    kubectl.kubernetes.io/last-applied-configuration: |
      {"apiVersion":"v1","kind":"ConfigMap","metadata":{"annotations":{},"labels":{"app.kubernetes.io/name":"argocd-rbac-cm","app.kubernetes.io/part-of":"argocd"},"name":"argocd-rbac-cm","namespace":"argocd"}}
  creationTimestamp: "2022-03-23T07:15:46Z"
[...]

En él he creado un role llamado org-admin que puede hacer la mayor parte de las operaciones, y se lo he asignado al grupo de usuarios argocd-admin y para el grupo argocd-readers he usado el role preconstruido llamado readonly.

Probar el entorno

Antes de probar los cambios que hemos hecho en nuestro entorno, lo primero que necesitas es reiniciar un par de despliegues:

# Restart deployment
kubectl rollout restart deployment argocd-server -n argocd
kubectl rollout restart deployment argocd-dex-server -n argocd

Una vez que finalicen estas dos operaciones, en mi ejemplo utilizo port forwarding al puerto 8080 ya que no tengo de manera pública el acceso a la UI:

# Port forward the ArgoCD server
kubectl port-forward svc/argocd-server -n argocd 8080:443

Si ahora accedes al portal comprobarás que la página de login ha cambiado y que ahora aparece un botón adicional que pone LOG IN VIA AZURE.

Página de inicio de sesión de Argo CD integrado con Azure Active Directory

Si inicias sesión con el usuario que está en el grupo argocd-admins podrás realizar todas las operaciones establecidas en el config map del RBAC sin problemas y si lo intentas con el usuario en el grupo argocd-readers verás que al intentarlo obtendrás un error.

Argo CD - error al intentar crear un proyecto con un usuario de Azure AD que no tiene permisos
Argo CD – error al intentar crear un proyecto con un usuario de Azure AD que no tiene permisos

¡Saludos!