본문 바로가기
GCP

workload identity federation with k8s cluster

by misankim 2023. 7. 2.

This post covers how to use Kubernetes as an OIDC provider role to use GCP's workload identity federation service by delegating an IAM role without a json key for your GCP service account.
 
It can be used when an application running on a Kubernetes cluster in a non-GCP environment wants to access GCP's services, or when an application running on a server in a non-GCP environment wants to access GCP's services.
 

 
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 k8s-oidc-pool \
    --location="global" \
    --description="workload identity pool for k8s" \
    --display-name="k8s" \
    --project=my-test-project

 
 
# create workload identity provider
 

gcloud iam workload-identity-pools providers create-oidc k8s-oidc-provider \
    --location="global" \
    --workload-identity-pool="k8s-oidc-pool" \
    --issuer-uri="https://example.34.107.196.145.nip.io/" \
    --attribute-mapping='google.subject=assertion.sub' \
    --project=my-test-project

 
 
(Note) Google must be able to publicly call the OIDC provider's metadata URL.
 
https://cloud.google.com/iam/docs/workload-identity-federation-with-other-providers?hl=ko#prepare
 
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
 
The IdP's OIDC metadata and JWK endpoints are publicly accessible over the Internet.
Google Cloud uses these endpoints to download the IdP's key set and use this key set to validate tokens.
 
If cloud armor is set on the top of K8s
-> User agent google-thirdparty-credentials, source IP 107.178.192.0/18 must be allowed.
 
Path where the call occurs during actual authentication
https://example.34.107.196.145.nip.io/.well-known/openid-configuration
https://example.34.107.196.145.nip.io/openid/v1/jwks
 

 

(Note) If the k8s cluster, which is the oidc provider, is in a private network, google cannot access the kube api server endpoint.

-> Set the "--issuer-uri" parameter to the default issuer url of the current K8S, and create a provider by including the jwk value of the cluster through the "--jwk-json-path" parameter.

-> Even without publicly accessing OIDC metadata and JWK endpoints, the user can obtain the sts federation token after JWT validation through the JWK entered when creating the provider.

 

https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/#service-account-issuer-discovery

 

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

 

https://cloud.google.com/sdk/gcloud/reference/iam/workload-identity-pools/providers/create-oidc#--jwk-json-path

 

ISSUER="$(kubectl get --raw /.well-known/openid-configuration | jq -r .issuer)"

kubectl get --raw /openid/v1/jwks > jwk.json

gcloud iam workload-identity-pools providers create-oidc k8s-oidc-provider \
    --location="global" \
    --workload-identity-pool="k8s-oidc-pool" \
    --issuer-uri=${ISSUER} \
    --allowed-audiences=${ISSUER} \
    --attribute-mapping='google.subject=assertion.sub' \
    --jwk-json-path=/k8s/jwk.json \
    --project=my-test-project
# check list of providers
gcloud iam workload-identity-pools providers list \
    --workload-identity-pool="k8s-oidc-pool" \
    --location="global" \
    --project=my-test-project

# output
---
attributeMapping:
  google.subject: assertion.sub
name: projects/123123123123/locations/global/workloadIdentityPools/k8s-oidc-pool/providers/k8s-oidc-provider
oidc:
  allowedAudiences:
  - https://kubernetes.default.svc.cluster.local
  issuerUri: https://kubernetes.default.svc.cluster.local/
  jwksJson: |
    {"keys":[{"use":"sig","kty":"RSA","kid":"0WGsJ1dI1epkGfo1B9xpygpfT0ddyu7P2pldHvjbbIw","alg":"RS256","n":"o6Za..(skip)..vDjXw","e":"AQAB"}]}
state: ACTIVE


 
# Grant the iam.workloadIdentityUser role so that sa in k8s can impersonate sa in gcp.
 
create sa

gcloud iam service-accounts create gcs-gsa \
    --project=my-test-project

 
Grant gcs permission to sa

gcloud projects add-iam-policy-binding my-test-project \
    --member "serviceAccount:gcs-gsa@my-test-project.iam.gserviceaccount.com" \
    --role "roles/storage.objectAdmin"

 
check project number

gcloud projects describe my-test-project

 
To allow only specific sa

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/123412341234/locations/global/workloadIdentityPools/k8s-oidc-pool/subject/system:serviceaccount:default:testsa" \
    --project=my-test-project

 
To allow all sa in k8s

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/123412341234/locations/global/workloadIdentityPools/k8s-oidc-pool/*" \
    --project=my-test-project

 
 
# k8s kube api server settings
-> Add option when running kube api server to operate oidc issuer through kube api server of k8s
-> Below are the options when running a cluster in the minikube environment.
 
https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/
 
https://kubernetes.io/docs/reference/command-line-tools-reference/kube-apiserver/
 

minikube start \
  --extra-config=apiserver.service-account-issuer=https://example.34.107.196.145.nip.io \
  --extra-config=apiserver.service-account-jwks-uri=https://example.34.107.196.145.nip.io/openid/v1/jwks \
  --extra-config=apiserver.api-audiences=https://iam.googleapis.com/projects/123412341234/locations/global/workloadIdentityPools/k8s-oidc-pool/providers/k8s-oidc-provider

 
 
# Bind a role to allow unauthenticated users to access the oidc metadata url endpoint
 
https://developer.hashicorp.com/vault/docs/auth/jwt/oidc-providers/kubernetes
https://techblog.cisco.com/blog/kubernetes-oidc
 

kubectl create clusterrolebinding oidc-reviewer --clusterrole=system:service-account-issuer-discovery --group=system:unauthenticated

 
check
https://example.34.107.196.145.nip.io/.well-known/openid-configuration
https://example.34.107.196.145.nip.io/openid/v1/jwks
 
output

{"issuer":"https://example.34.107.196.145.nip.io","jwks_uri":"https://example.34.107.196.145.nip.io/openid/v1/jwks","response_types_supported":["id_token"],"subject_types_supported":["public"],"id_token_signing_alg_values_supported":["RS256"]}
{"keys":[{"use":"sig","kty":"RSA","kid":"0WGsJ1dI1epkGfo1B9xpygpfT0ddyu7P2pldHvjbbIw","alg":"RS256","n":"o6Za..(중략)..vDjXw","e":"AQAB"}]}

 
The information displayed is the same as the result of the command below.

kubectl get --raw /.well-known/openid-configuration
kubectl get --raw /openid/v1/jwks

 
oidc metadata url related k8s cluster role

kubectl get clusterrole system:service-account-issuer-discovery -o yaml

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  annotations:
    rbac.authorization.kubernetes.io/autoupdate: "true"
  creationTimestamp: "2023-06-02T23:09:01Z"
  labels:
    kubernetes.io/bootstrapping: rbac-defaults
  name: system:service-account-issuer-discovery
  resourceVersion: "100"
  uid: 22cba2f0-0ebb-45a8-a7f7-627315170933
rules:
- nonResourceURLs:
  - /.well-known/openid-configuration
  - /openid/v1/jwks
  verbs:
  - get

 
 
# create k8s sa

kubectl create sa testsa

kubectl apply -f - <<EOF
apiVersion: v1
kind: Secret
metadata:
  name: testsa.service-account-token
  annotations:
    kubernetes.io/service-account.name: testsa
type: kubernetes.io/service-account-token
EOF

 
 
# Load oidc jwt token for k8s sa

 
https://kubernetes.io/docs/reference/access-authn-authz/authentication/#service-account-tokens
 

AUTH_TOKEN="$(kubectl create token testsa)"
echo $AUTH_TOKEN > token.txt

 
Below is the payload of the jwt token confirmed on the https://jwt.io/ site.

{
  "aud": [
    "https://iam.googleapis.com/projects/123412341234/locations/global/workloadIdentityPools/k8s-oidc-pool/providers/k8s-oidc-provider"
  ],
  "exp": 1688041864,
  "iat": 1688038264,
  "iss": "https://example.34.107.196.145.nip.io",
  "kubernetes.io": {
    "namespace": "default",
    "serviceaccount": {
      "name": "testsa",
      "uid": "b2d963aa-ea13-48d7-aa28-c754a3999789"
    }
  },
  "nbf": 1688038264,
  "sub": "system:serviceaccount:default:testsa"
}

 
(Note) When creating a sa token, you can set the token validity period with the --duration option (if there is no option, the default is 1 hour)
 

kubectl create token testsa --duration 24h

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

gcloud iam workload-identity-pools create-cred-config \
projects/123412341234/locations/global/workloadIdentityPools/k8s-oidc-pool/providers/k8s-oidc-provider \
  --service-account="gcs-gsa@my-test-project.iam.gserviceaccount.com" \
  --credential-source-type="text" \
  --output-file=sts-creds.json \
  --credential-source-file=/k8s/token.txt

 
Set as environment variable

export GOOGLE_APPLICATION_CREDENTIALS=/k8s/sts-creds.json

 
Contents of the generated sts-creds.json file

{
  "type": "external_account",
  "audience": "//iam.googleapis.com/projects/123412341234/locations/global/workloadIdentityPools/k8s-oidc-pool/providers/k8s-oidc-provider",
  "subject_token_type": "urn:ietf:params:oauth:token-type:jwt",
  "token_url": "https://sts.googleapis.com/v1/token",
  "credential_source": {
    "file": "/k8s/token.txt",
    "format": {
      "type": "text"
    }
  },
  "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/k8s-oidc-pool"

 

{
  "protoPayload": {
    "@type": "type.googleapis.com/google.cloud.audit.AuditLog",
    "status": {},
    "authenticationInfo": {
      "principalSubject": "system:serviceaccount:default:testsa"
    },
    "requestMetadata": {
      "callerIp": "123.123.123.123",
      "callerSuppliedUserAgent": "python-requests/2.28.2,gzip(gfe)",
      "requestAttributes": {
        "time": "2023-06-29T11:41:12.006985157Z",
        "auth": {}
      },
      "destinationAttributes": {}
    },
    "serviceName": "sts.googleapis.com",
    "methodName": "google.identity.sts.v1.SecurityTokenService.ExchangeToken",
    "authorizationInfo": [
      {
        "permission": "sts.identityProviders.checkLogging"
      }
    ],
    "resourceName": "projects/123412341234/locations/global/workloadIdentityPools/k8s-oidc-pool/providers/k8s-oidc-provider",
    "request": {
      "grantType": "urn:ietf:params:oauth:grant-type:token-exchange",
      "subjectTokenType": "urn:ietf:params:oauth:token-type:jwt",
      "audience": "//iam.googleapis.com/projects/123412341234/locations/global/workloadIdentityPools/k8s-oidc-pool/providers/k8s-oidc-provider",
      "@type": "type.googleapis.com/google.identity.sts.v1.ExchangeTokenRequest",
      "requestedTokenType": "urn:ietf:params:oauth:token-type:access_token"
    },
    "metadata": {
      "mapped_principal": "principal://iam.googleapis.com/projects/123412341234/locations/global/workloadIdentityPools/k8s-oidc-pool/subject/system:serviceaccount:default:testsa",
      "@type": "type.googleapis.com/google.identity.sts.v1.AuditData"
    }
  },
  "insertId": "1kw8xp1cb6o",
  "resource": {
    "type": "audited_resource",
    "labels": {
      "service": "sts.googleapis.com",
      "method": "google.identity.sts.v1.SecurityTokenService.ExchangeToken",
      "project_id": "my-test-project"
    }
  },
  "timestamp": "2023-06-29T11:41:11.999378987Z",
  "severity": "INFO",
  "logName": "projects/my-test-project/logs/cloudaudit.googleapis.com%2Fdata_access",
  "receiveTimestamp": "2023-06-29T11:41:12.680063691Z"
}

 
## 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/123412341234/locations/global/workloadIdentityPools/k8s-oidc-pool/subject/system:serviceaccount:default:testsa"
        }
      ]
    },
    "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-29T11:41:13.350663952Z",
        "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": "qn7rt1e5m940",
  "resource": {
    "type": "gcs_bucket",
    "labels": {
      "bucket_name": "my-test-bucket",
      "location": "asia-northeast3",
      "project_id": "my-test-project"
    }
  },
  "timestamp": "2023-06-29T11:41:13.343141622Z",
  "severity": "INFO",
  "logName": "projects/my-test-project/logs/cloudaudit.googleapis.com%2Fdata_access",
  "receiveTimestamp": "2023-06-29T11:41:14.440941103Z"
}

 
 
# Issuance of temporary tokens through permanent tokens
-> Since the token created through the "kubectl create token SA_NAME" command is a temporary token, you can include a permanent token in the application and request a temporary token from the kube api server when needed.
-> In the case of persistent tokens, the token specifications are different and cannot be used for workload identity federation.
-> sa(token-creator) for permanent tokens and sa(testsa) to create temporary tokens can be used separately (define sa to allow token creation in the rules.[0].resourceNames path of Role)
 
## Create/grant sa and role for token creation
 

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: token-creator
  namespace: default
rules:
- apiGroups:
  - ""
  resources:
    - serviceaccounts/token
  resourceNames:
    - testsa
  verbs:
    - create
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: token-creator
  namespace: default
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: Role
  name: token-creator
subjects:
- kind: ServiceAccount
  name: token-creator
  namespace: default
---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: token-creator
  namespace: default
---
apiVersion: v1
kind: Secret
metadata:
  name: token-creator.service-account-token
  namespace: default
  annotations:
    kubernetes.io/service-account.name: token-creator
type: kubernetes.io/service-account-token

 
## Define token creation request
 
https://kubernetes.io/docs/reference/kubernetes-api/authentication-resources/token-request-v1/
 

vim token_request.json

{
  "kind": "TokenRequest",
  "apiVersion": "authentication.k8s.io/v1",
  "metadata": {
    "creationTimestamp": null
  },
  "spec": {
    "audiences": null,
    "expirationSeconds": null,
    "boundObjectRef": null
  },
  "status": {
    "token": "",
    "expirationTimestamp": null
  }
}

 
## Token creation request
 

APISERVER="https://example.34.107.196.145.nip.io"
SERVICEACCOUNT="testsa"
NAMESPACE="default"
TOKEN="$(kubectl get secret token-creator.service-account-token -o=jsonpath='{.data.token}' | base64 --decode)"

curl -vk -H "content-type: application/json" -H "Authorization: Bearer ${TOKEN}" \
  -d @token_request.json -X POST "${APISERVER}/api/v1/namespaces/${NAMESPACE}/serviceaccounts/${SERVICEACCOUNT}/token"

 
Create a token and save it as a token.txt file.

curl -sk -H "content-type: application/json" -H "Authorization: Bearer ${TOKEN}" \
  -d @token_request.json -X POST "${APISERVER}/api/v1/namespaces/${NAMESPACE}/serviceaccounts/${SERVICEACCOUNT}/token" | jq -r .status.token > token.txt