Claude
Skills
Sign in
Back

harness-keycloak-auth

Included with Lifetime
$97 forever

Keycloak OIDC integration with Harness pipelines, EKS IRSA, service account authentication, and realm-as-code patterns

General

What this skill does


# Harness Keycloak Auth Skill

Integrate Keycloak OIDC with Harness pipelines and EKS deployments.

## Use For
- Keycloak client management in pipelines, OIDC for EKS authentication
- Realm-as-code patterns, service account provisioning

## Keycloak Helm Values for EKS

```yaml
# charts/keycloak/values.yaml
keycloak:
  replicas: 2

  image:
    repository: quay.io/keycloak/keycloak
    tag: "24.0.1"

  command:
    - "/opt/keycloak/bin/kc.sh"
    - "start"
    - "--optimized"

  extraEnv: |
    - name: KC_HOSTNAME
      value: "keycloak.{{ .Values.global.domain }}"
    - name: KC_PROXY
      value: "edge"
    - name: KC_DB
      value: "postgres"
    - name: KC_DB_URL
      valueFrom:
        secretKeyRef:
          name: keycloak-db
          key: url
    - name: KC_DB_USERNAME
      valueFrom:
        secretKeyRef:
          name: keycloak-db
          key: username
    - name: KC_DB_PASSWORD
      valueFrom:
        secretKeyRef:
          name: keycloak-db
          key: password
    - name: KEYCLOAK_ADMIN
      valueFrom:
        secretKeyRef:
          name: keycloak-admin
          key: username
    - name: KEYCLOAK_ADMIN_PASSWORD
      valueFrom:
        secretKeyRef:
          name: keycloak-admin
          key: password

  ingress:
    enabled: true
    ingressClassName: nginx
    annotations:
      cert-manager.io/cluster-issuer: letsencrypt-prod
    rules:
      - host: keycloak.{{ .Values.global.domain }}
        paths:
          - path: /
            pathType: Prefix
    tls:
      - secretName: keycloak-tls
        hosts:
          - keycloak.{{ .Values.global.domain }}

  serviceAccount:
    create: true
    annotations:
      eks.amazonaws.com/role-arn: arn:aws:iam::{{ .Values.aws.accountId }}:role/keycloak-role
```

## Realm-as-Code Configuration

### Realm Export Template
```json
{
  "realm": "{{ .Values.keycloak.realm }}",
  "enabled": true,
  "sslRequired": "external",
  "registrationAllowed": false,
  "loginWithEmailAllowed": true,
  "duplicateEmailsAllowed": false,
  "resetPasswordAllowed": true,
  "editUsernameAllowed": false,
  "bruteForceProtected": true,
  "permanentLockout": false,
  "maxFailureWaitSeconds": 900,
  "minimumQuickLoginWaitSeconds": 60,
  "waitIncrementSeconds": 60,
  "quickLoginCheckMilliSeconds": 1000,
  "maxDeltaTimeSeconds": 43200,
  "failureFactor": 5,
  "roles": {
    "realm": [
      { "name": "admin", "description": "Administrator" },
      { "name": "user", "description": "Standard user" }
    ]
  },
  "clients": [],
  "users": [],
  "browserSecurityHeaders": {
    "contentSecurityPolicyReportOnly": "",
    "xContentTypeOptions": "nosniff",
    "xRobotsTag": "none",
    "xFrameOptions": "SAMEORIGIN",
    "contentSecurityPolicy": "frame-src 'self'; frame-ancestors 'self'; object-src 'none';",
    "xXSSProtection": "1; mode=block",
    "strictTransportSecurity": "max-age=31536000; includeSubDomains"
  }
}
```

### Client Configuration Template
```json
{
  "clientId": "{{ .Values.service.name }}-client",
  "name": "{{ .Values.service.name }} Service",
  "enabled": true,
  "clientAuthenticatorType": "client-secret",
  "secret": "{{ .Values.keycloak.clientSecret }}",
  "redirectUris": [
    "https://{{ .Values.service.name }}.{{ .Values.global.domain }}/*"
  ],
  "webOrigins": [
    "https://{{ .Values.service.name }}.{{ .Values.global.domain }}"
  ],
  "standardFlowEnabled": true,
  "implicitFlowEnabled": false,
  "directAccessGrantsEnabled": true,
  "serviceAccountsEnabled": true,
  "publicClient": false,
  "protocol": "openid-connect",
  "attributes": {
    "pkce.code.challenge.method": "S256",
    "access.token.lifespan": "300",
    "client.session.idle.timeout": "1800"
  },
  "defaultClientScopes": [
    "web-origins",
    "profile",
    "roles",
    "email"
  ],
  "optionalClientScopes": [
    "address",
    "phone",
    "offline_access"
  ]
}
```

## Harness Pipeline Steps for Keycloak

### Create/Update Keycloak Client
```yaml
- step:
    type: Run
    name: Configure Keycloak Client
    identifier: configure_keycloak
    spec:
      shell: Bash
      command: |
        # Get admin token
        TOKEN=$(curl -s -X POST \
          "${KEYCLOAK_URL}/realms/master/protocol/openid-connect/token" \
          -H "Content-Type: application/x-www-form-urlencoded" \
          -d "username=${KEYCLOAK_ADMIN}" \
          -d "password=${KEYCLOAK_ADMIN_PASSWORD}" \
          -d "grant_type=password" \
          -d "client_id=admin-cli" | jq -r '.access_token')

        # Check if client exists
        CLIENT_ID="<+service.name>-client"
        EXISTING=$(curl -s -X GET \
          "${KEYCLOAK_URL}/admin/realms/${KEYCLOAK_REALM}/clients?clientId=${CLIENT_ID}" \
          -H "Authorization: Bearer ${TOKEN}" | jq -r '.[0].id // empty')

        # Prepare client config
        cat > /tmp/client.json << 'EOF'
        {
          "clientId": "<+service.name>-client",
          "name": "<+service.name>",
          "enabled": true,
          "clientAuthenticatorType": "client-secret",
          "redirectUris": ["https://<+service.name>.<+pipeline.variables.domain>/*"],
          "webOrigins": ["https://<+service.name>.<+pipeline.variables.domain>"],
          "standardFlowEnabled": true,
          "serviceAccountsEnabled": true,
          "publicClient": false,
          "protocol": "openid-connect"
        }
        EOF

        if [ -n "$EXISTING" ]; then
          # Update existing client
          curl -s -X PUT \
            "${KEYCLOAK_URL}/admin/realms/${KEYCLOAK_REALM}/clients/${EXISTING}" \
            -H "Authorization: Bearer ${TOKEN}" \
            -H "Content-Type: application/json" \
            -d @/tmp/client.json
          echo "Updated client: ${CLIENT_ID}"
        else
          # Create new client
          curl -s -X POST \
            "${KEYCLOAK_URL}/admin/realms/${KEYCLOAK_REALM}/clients" \
            -H "Authorization: Bearer ${TOKEN}" \
            -H "Content-Type: application/json" \
            -d @/tmp/client.json
          echo "Created client: ${CLIENT_ID}"
        fi

        # Get client secret
        CLIENT_UUID=$(curl -s -X GET \
          "${KEYCLOAK_URL}/admin/realms/${KEYCLOAK_REALM}/clients?clientId=${CLIENT_ID}" \
          -H "Authorization: Bearer ${TOKEN}" | jq -r '.[0].id')

        SECRET=$(curl -s -X GET \
          "${KEYCLOAK_URL}/admin/realms/${KEYCLOAK_REALM}/clients/${CLIENT_UUID}/client-secret" \
          -H "Authorization: Bearer ${TOKEN}" | jq -r '.value')

        # Store in AWS Secrets Manager
        aws secretsmanager put-secret-value \
          --secret-id "<+service.name>/keycloak-client-secret" \
          --secret-string "${SECRET}"
      envVariables:
        KEYCLOAK_URL: <+pipeline.variables.keycloak_url>
        KEYCLOAK_REALM: <+pipeline.variables.keycloak_realm>
        KEYCLOAK_ADMIN: <+secrets.getValue("keycloak_admin_user")>
        KEYCLOAK_ADMIN_PASSWORD: <+secrets.getValue("keycloak_admin_password")>
```

### Realm Import Step
```yaml
- step:
    type: Run
    name: Import Keycloak Realm
    identifier: import_realm
    spec:
      shell: Bash
      command: |
        # Get admin token
        TOKEN=$(curl -s -X POST \
          "${KEYCLOAK_URL}/realms/master/protocol/openid-connect/token" \
          -H "Content-Type: application/x-www-form-urlencoded" \
          -d "username=${KEYCLOAK_ADMIN}" \
          -d "password=${KEYCLOAK_ADMIN_PASSWORD}" \
          -d "grant_type=password" \
          -d "client_id=admin-cli" | jq -r '.access_token')

        # Import realm from repo
        curl -s -X POST \
          "${KEYCLOAK_URL}/admin/realms" \
          -H "Authorization: Bearer ${TOKEN}" \
          -H "Content-Type: application/json" \
          -d @keycloak/realm-export.json

        echo "Realm imported successfully"
```

## EKS OIDC Integration with Keycloak

### IRSA for Keycloak-Authenticated Pods
```yaml
# ServiceAccount with Keycloak token injection
apiVersion: v1
kind: ServiceAccount
metadata:
  name: {{ .Values.service.name

Related in General