GCP

workload identity federation with keycloak

misankim 2023. 7. 9. 20:26

workload identity federation with keycloak
-> Authenticate without gcp service account json key by linking keycloak with oidc provider


workload identity federation
https://cloud.google.com/iam/docs/workload-identity-federation

Integrate Cloud Run and workload identity federation
https://cloud.google.com/iam/docs/tutorial-cloud-run-workload-id-federation

Configure workload identity federation with other identity providers
https://cloud.google.com/iam/docs/workload-identity-federation-with-other-providers


# workload identity federation
-> A way to grant authenticated users permission to impersonate gcp's mapped sa through a trusted external certification authority.
-> Since it is a means of authenticating outside of gcp without a json key, there is no concern about key management or leakage.

 



# create workload identity pool

gcloud iam workload-identity-pools create keycloak-oidc-pool \
    --location="global" \
    --description="workload identity pool for keycloak" \
    --display-name="keycloak" \
    --project=my-test-project



# workload identity provider 생성
-> The keycloak realm was created based on the master realm, but can be changed to another realm if necessary.
-> Map preferred_username to subject
-> Assume all users of my-keycloak.example.com are trusted users and do not set conditions.

gcloud iam workload-identity-pools providers create-oidc keycloak \
    --location="global" \
    --workload-identity-pool="keycloak-oidc-pool" \
    --issuer-uri="https://my-keycloak.example.com/auth/realms/master" \
    --allowed-audiences="account" \
    --attribute-mapping='google.subject=assertion.preferred_username' \
    --project=my-test-project

 

 

(참고) Google must be able to publicly call the OIDC provider's metadata URL.

https://cloud.google.com/iam/docs/troubleshooting-workload-identity-federation?hl=ko#error-connecting-issuer 

https://cloud.google.com/iam/docs/configuring-workload-identity-federation?hl=ko#oidc 

If cloud armor is set at the top of keycloak  
-> User agent google-thirdparty-credentials, source IP 107.178.192.0/18 must be allowed.

Path where the call occurs during actual authentication
-> In keycloak version 17.0 and higher, /auth/ path is omitted.

https://my-keycloak.example.com/auth/realms/master/.well-known/openid-configuration
https://my-keycloak.example.com/auth/realms/master/protocol/openid-connect/certs

 

 

# Grant the iam.workloadIdentityUser role so that the keycloak object or group can impersonate gcp sa

Check project number

gcloud projects describe my-test-project



To allow only specific users

gcloud iam service-accounts add-iam-policy-binding gcs-gsa@my-test-project.iam.gserviceaccount.com \
    --role=roles/iam.workloadIdentityUser \
    --member="principal://iam.googleapis.com/projects/123123123123/locations/global/workloadIdentityPools/keycloak-oidc-pool/subject/user1" \
    --project=my-test-project

 

To allow all users of keycloak

gcloud iam service-accounts add-iam-policy-binding gcs-gsa@my-test-project.iam.gserviceaccount.com \
    --role=roles/iam.workloadIdentityUser \
    --member="principalSet://iam.googleapis.com/projects/123123123123/locations/global/workloadIdentityPools/keycloak-oidc-pool/*" \
    --project=my-test-project

 

 

# create keycloak client

master realm -> Configure -> Clients -> Create

Client ID - google-oidc
Client Protocol - openid-connect
Access Type - confidential(로그인 시 클라이언트 시크릿 필요)
Valid Redirect URIs - http://localhost/
-> It is not necessary as it will not be linked to the web page, but it is a required value so enter it (if it can be left blank depending on the keycloak version)

 

In keycloak version 21 or higher, the Access Type setting must be set to Capability config -> Client authentication to enable the credentials tab at the top to check the client secret.

 

 

# create keycloak gruop and user

master realm -> Manage -> Groups -> New -> Create appropriate groups
master realm -> Manage -> Users -> Add user
Username - user1 -> assertion.preferred_username value to map to google.subject

After creating a user, change the Credentials tab -> Set Password section to Temporary -> OFF and specify a new password (to be used to request tokens)

 

 

# Fetch oidc jwt token for keycloak user
-> The generated jwt token can be verified at https://jwt.io/ site.
-> Tokens generated by keycloak are only valid for 1 minute
-> In keycloak version 17.0 or higher, the /auth/ path must be omitted.

curl -s -L -X POST 'https://my-keycloak.example.com/auth/realms/master/protocol/openid-connect/token' \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  --data-urlencode 'client_id=google-oidc' \
  --data-urlencode 'grant_type=password' \
  --data-urlencode 'client_secret=GUmKphDbiaifJJXj6penj6vqKDvXbDal' \
  --data-urlencode 'scope=openid' \
  --data-urlencode 'username=user1' \
  --data-urlencode 'password=user1pass' > /root/keycloak/token.json

 

Contents of the generated token file

{"access_token":"eyJh..(skip)..QpEA","expires_in":60,"refresh_expires_in":1800,"refresh_token":"eyJh..(skip)..c2ak","token_type":"Bearer","id_token":"eyJh..(skip)..-mKQ","not-before-policy":0,"session_state":"c6880fef-8549-45cb-af1d-0d87755d52b8","scope":"openid profile email"}

 

 

# Set the GOOGLE_APPLICATION_CREDENTIALS environment variable to authenticate using workload identity federation
-> The GOOGLE_APPLICATION_CREDENTIALS authentication file contains the path to the file containing the keycloak token information, and authenticates through the token.

Create authentication file

gcloud iam workload-identity-pools create-cred-config \
projects/123123123123/locations/global/workloadIdentityPools/keycloak-oidc-pool/providers/keycloak \
  --service-account="gcs-gsa@my-test-project.iam.gserviceaccount.com" \
  --credential-source-field-name="access_token" \
  --credential-source-type="json" \
  --output-file=sts-creds.json \
  --credential-source-file=/root/keycloak/token.json



Set as environment variable

export GOOGLE_APPLICATION_CREDENTIALS=/root/keycloak/sts-creds.json



Contents of the generated sts-creds.json file

{
  "type": "external_account",
  "audience": "//iam.googleapis.com/projects/123123123123/locations/global/workloadIdentityPools/keycloak-oidc-pool/providers/keycloak",
  "subject_token_type": "urn:ietf:params:oauth:token-type:jwt",
  "token_url": "https://sts.googleapis.com/v1/token",
  "credential_source": {
    "file": "/root/keycloak/token.json",
    "format": {
      "type": "json",
      "subject_token_field_name": "access_token"
    }
  },
  "service_account_impersonation_url": "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/gcs-gsa@my-test-project.iam.gserviceaccount.com:generateAccessToken"
}

 

 

# Write sample code

vim gcs-list.py

from google.cloud import storage

project_id = "my-test-project"
bucket_name = "my-test-bucket"

storage_client = storage.Client(project=project_id)
blobs = storage_client.list_blobs(bucket_name)

for blob in blobs:
    print(blob.name)

 

Install gcs python client library

pip install google-cloud-storage

 

Run sample code

python gcs-list.py

 

 

# Check log

## workload identity federation authentication log

log query

resource.type="audited_resource" resource.labels.service="sts.googleapis.com" protoPayload.resourceName:"workloadIdentityPools/keycloak-oidc-pool"

 

{
  "protoPayload": {
    "@type": "type.googleapis.com/google.cloud.audit.AuditLog",
    "status": {},
    "authenticationInfo": {
      "principalSubject": "41a4d4fd-b761-4efa-9eb2-6eb792a0321b"
    },
    "requestMetadata": {
      "callerIp": "123.123.123.123",
      "callerSuppliedUserAgent": "python-requests/2.28.2,gzip(gfe)",
      "requestAttributes": {
        "time": "2023-06-15T14:57:48.613209504Z",
        "auth": {}
      },
      "destinationAttributes": {}
    },
    "serviceName": "sts.googleapis.com",
    "methodName": "google.identity.sts.v1.SecurityTokenService.ExchangeToken",
    "authorizationInfo": [
      {
        "permission": "sts.identityProviders.checkLogging"
      }
    ],
    "resourceName": "projects/123123123123/locations/global/workloadIdentityPools/keycloak-oidc-pool/providers/keycloak",
    "request": {
      "audience": "//iam.googleapis.com/projects/123123123123/locations/global/workloadIdentityPools/keycloak-oidc-pool/providers/keycloak",
      "grantType": "urn:ietf:params:oauth:grant-type:token-exchange",
      "requestedTokenType": "urn:ietf:params:oauth:token-type:access_token",
      "@type": "type.googleapis.com/google.identity.sts.v1.ExchangeTokenRequest",
      "subjectTokenType": "urn:ietf:params:oauth:token-type:jwt"
    },
    "metadata": {
      "@type": "type.googleapis.com/google.identity.sts.v1.AuditData",
      "mapped_principal": "principal://iam.googleapis.com/projects/123123123123/locations/global/workloadIdentityPools/keycloak-oidc-pool/subject/user1"
    }
  },
  "insertId": "nbmtg0caj8",
  "resource": {
    "type": "audited_resource",
    "labels": {
      "method": "google.identity.sts.v1.SecurityTokenService.ExchangeToken",
      "service": "sts.googleapis.com",
      "project_id": "my-test-project"
    }
  },
  "timestamp": "2023-06-15T14:57:48.603771403Z",
  "severity": "INFO",
  "logName": "projects/my-test-project/logs/cloudaudit.googleapis.com%2Fdata_access",
  "receiveTimestamp": "2023-06-15T14:57:49.145786402Z"
}

 

## gcs bucket object list call log

-> Check if the call was made to the sa account (gcs-gsa@my-test-project.iam.gserviceaccount.com) set by impersonate.

{
  "protoPayload": {
    "@type": "type.googleapis.com/google.cloud.audit.AuditLog",
    "status": {},
    "authenticationInfo": {
      "principalEmail": "gcs-gsa@my-test-project.iam.gserviceaccount.com",
      "serviceAccountDelegationInfo": [
        {
          "principalSubject": "principal://iam.googleapis.com/projects/123123123123/locations/global/workloadIdentityPools/terraform-cloud/subject/user1"
        }
      ]
    },
    "requestMetadata": {
      "callerIp": "123.123.123.123",
      "callerSuppliedUserAgent": "gcloud-python/2.9.0  gl-python/3.11.2 grpc/1.51.3 gax/2.11.0 gccl/2.9.0,gzip(gfe)",
      "requestAttributes": {
        "time": "2023-06-15T14:57:49.933703195Z",
        "auth": {}
      },
      "destinationAttributes": {}
    },
    "serviceName": "storage.googleapis.com",
    "methodName": "storage.objects.list",
    "authorizationInfo": [
      {
        "resource": "projects/_/buckets/my-test-bucket",
        "permission": "storage.objects.list",
        "granted": true,
        "resourceAttributes": {}
      }
    ],
    "resourceName": "projects/_/buckets/my-test-bucket",
    "resourceLocation": {
      "currentLocations": [
        "asia-northeast3"
      ]
    }
  },
  "insertId": "1o2r1be193j2",
  "resource": {
    "type": "gcs_bucket",
    "labels": {
      "location": "asia-northeast3",
      "project_id": "my-test-project",
      "bucket_name": "my-test-bucket"
    }
  },
  "timestamp": "2023-06-15T14:57:49.925772204Z",
  "severity": "INFO",
  "logName": "projects/my-test-project/logs/cloudaudit.googleapis.com%2Fdata_access",
  "receiveTimestamp": "2023-06-15T14:57:50.653213163Z"
}