workload identity federation with keycloak
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"
}