visit
But what if you are running your application workload on GKE cluster and
would like to access AWS services without compromising on security?
Let’s assume that you already have an AWS account, and a GKE cluster and your company has decided to run a microservice-based application on GKE
cluster, but still wants to use resources in the AWS account (Amazon S3 and SNS services) to integrate with other systems deployed on AWS.
For example, the orchestration job (deployed as a Kubernetes Job) is running inside a GKE cluster and needs to upload a data file into a S3 bucket and send a message to an Amazon SNS topic. The equivalent command-line might be:
aws s3 cp data.csv s3://my-data-bucket/datagram_12345.csv
aws sns publish --topic-arn arn:aws:sns:us-west-2:2:my-data-topic --message "datagram_12345.csv apply geo-filter"
Pretty simple example. In order for these commands to succeed, the orchestration job must have AWS credentials available to it, and those credentials must be able to make the relevant API calls.
Export AWS Access Key and Secret Key for some AWS IAM User, and inject AWS credentials into the orchestration job, either as a credentials file or environment variables. Probably not doing this directly, but using resource protected with .
The risk here is that these credentials never expire. They have to be
transferred somehow from the AWS environment to the GCP environment, and in most cases, people want them to be stored somewhere so that they can be used to re-create the orchestration job later if required.
When using long-term AWS credentials, there are multiple ways that your AWS account can be compromised; unintentionally committing AWS credentials into a GitHub repository, keeping them in a Wiki system, reusing credentials for different services and applications, allowing
non-restricted access and, .
While it’s possible to design a proper credentials management solution for
issued IAM User credentials, it won’t be required if you will never create these long-term credentials in the first place.
The basic idea is to assign to GKE Pod, similarly to Workload Identity and EKS IAM Roles for Service Accounts cloud-specific features.
Luckily for us, AWS allows to create an IAM role for OpenID Connect Federation identity providers instead of IAM users. On the other hand, Google implements OIDC provider and integrates it tightly with GKE through Workload Identity feature. Providing a valid OIDC token to GKE pod, running under Kubernetes Service Account linked to a Google Cloud Service Account. All these may come in handy to implement GKE-to-AWS secure access.
There is one thing missing, required to complete the puzzle. With properly setup Workflow Identity GKE Pod gets an OIDC access token that allows access to Google Cloud services. In order to get temporary AWS credentials from AWS Security Token Service (), you need to provide a valid OIDC ID token.
AWS SDK (and
aws-cli
tool) will automatically request temporary AWS credentials from STS service, when the following environment variables are properly setup:AWS_WEB_IDENTITY_TOKEN_FILE
- the path to the web identity token file (OIDC ID token)AWS_ROLE_ARN
- the of the role to assume by Pod containersAWS_ROLE_SESSION_NAME
- the name applied to this assume-role sessionThe
gtoken-webhook
is a Kubernetes mutating admission webhook, that mutates any K8s Pod running under specially annotated Kubernetes Service Account (see details below).The
gtoken-webhook
injects a gtoken
initContainer
into a target Pod and an additional gtoken
sidekick container (to refresh an OIDC ID token a moment before the expiration), mounts token volume and injects three AWS-specific environment variables. The gtoken
container generates a valid GCP OIDC ID Token and writes it to the token volume. It also injects required AWS environment variables.The AWS SDK will automatically make the corresponding
AssumeRoleWithWebIdentity
calls to AWS STS on your behalf. It will handle in-memory caching as well as refreshing credentials as needed.[...]
args:
[...]
- --tls-cert-file=/etc/webhook/certs/cert.pem
- --tls-private-key-file=/etc/webhook/certs/key.pem
volumeMounts:
- name: webhook-certs
mountPath: /etc/webhook/certs
readOnly: true
[...]
volumes:
- name: webhook-certs
secret:
secretName: gtoken-webhook-certs
./deployment/webhook-create-signed-cert.sh
creating certs in tmpdir /var/folders/vl/gxsw2kf13jsf7s8xrqzcybb00000gp/T/tmp.xsatrckI71
Generating RSA private key, 2048 bit long modulus
.........................+++
....................+++
e is 65537 (0x10001)
certificatesigningrequest.certificates.k8s.io/gtoken-webhook-svc.default created
NAME AGE REQUESTOR CONDITION
gtoken-webhook-svc.default 1s [email protected] Pending
certificatesigningrequest.certificates.k8s.io/gtoken-webhook-svc.default approved
secret/gtoken-webhook-certs configured
Once the secret is created, we can create a deployment and service.
These are standard Kubernetes deployment and service resources. Up until this point we’ve produced nothing but an HTTP server that’s accepting
requests through service on port
443
:kubectl create -f deployment/deployment.yaml
kubectl create -f deployment/service.yaml
Now that our webhook server is running, it can accept requests from the
apiserver
. However, we should create some configuration resources in Kubernetes first. Let’s start with our validating webhook, then we’ll configure the mutating webhook later. If you take a look at the , you’ll notice that it contains a placeholder for CA_BUNDLE
:[...]
service:
name: gtoken-webhook-svc
namespace: default
path: "/pods"
caBundle: ${CA_BUNDLE}
[...]
There is a that substitutes the
CA_BUNDLE
placeholder in the configuration with this CA. Run this command before creating the validating webhook configuration:cat ./deployment/mutatingwebhook.yaml | ./deployment/webhook-patch-ca-bundle.sh > ./deployment/mutatingwebhook-bundle.yaml
kubectl create -f deployment/mutatingwebhook-bundle.yaml
Create Kubernetes Service Account to be used with
gtoken-webhook
:kubectl create -f deployment/service-account.yaml
# create a cluster role
kubectl create -f deployment/clusterrole.yaml
# define a cluster role binding
kubectl create 0f deployment/clusterrolebinding.yaml
Some of the following variables should be provided by the user, others will
be automatically generated and reused in the following steps.
PROJECT_ID
- GCP project ID (provided by the user)CLUSTER_NAME
- GKE cluster name (provided by the user)GSA_NAME
- Google Cloud Service Account name (provided by the user)GSA_ID
- Google Cloud Service Account unique ID (generated by Google)KSA_NAME
- Kubernetes Service Account name (provided by the user)KSA_NAMESPACE
- Kubernetes namespace (provided by the user)AWS_ROLE_NAME
- AWS IAM role name (provided by the user)AWS_POLICY_NAME
- an AWS IAM policy to assign to IAM role (provided by the user)AWS_ROLE_ARN
- AWS IAM Role ARN identifier (generated by AWS)gcloud beta container clusters create ${CLUSTER_NAME} --identity-namespace=${PROJECT_ID}.svc.id.goog
gcloud beta container clusters update ${CLUSTER_NAME} --identity-namespace=${PROJECT_ID}.svc.id.goog
# create GCP Service Account
gcloud iam service-accounts create ${GSA_NAME}
# get GCP SA UID to be used for AWS Role with Google OIDC Web Identity
GSA_ID=$(gcloud iam service-accounts describe --format json ${GSA_NAME}@${PROJECT_ID}.iam.gserviceaccount.com | jq -r '.uniqueId')
Update
GSA_NAME
Google Service Account with following roles:roles/iam.workloadIdentityUser
- impersonate service accounts from GKE Workloadsroles/iam.serviceAccountTokenCreator
- impersonate service accounts to create OAuth2 access tokens, sign blobs, or sign JWT tokensgcloud iam service-accounts add-iam-policy-binding \
--role roles/iam.workloadIdentityUser \
--role roles/iam.serviceAccountTokenCreator \
--member "serviceAccount:${PROJECT_ID}.svc.id.goog[${K8S_NAMESPACE}/${KSA_NAME}]" \
${GSA_NAME}@${PROJECT_ID}.iam.gserviceaccount.com
cat > gcp-trust-policy.json << EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Federated": "accounts.google.com"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"accounts.google.com:sub": "${GSA_SA}"
}
}
}
]
}
EOF
aws iam create-role --role-name ${AWS_ROLE_NAME} --assume-role-policy-document file://gcp-trust-policy.json
aws iam attach-role-policy --role-name ${AWS_ROLE_NAME} --policy-arn arn:aws:iam::aws:policy/${AWS_POLICY_NAME}
AWS_ROLE_ARN=$(aws iam get-role --role-name ${ROLE_NAME} --query Role.Arn --output text)
kubectl create namespace ${K8S_NAMESPACE}
kubectl create serviceaccount --namespace ${K8S_NAMESPACE} ${KSA_NAME}
kubectl annotate serviceaccount --namespace ${K8S_NAMESPACE} ${KSA_NAME}
iam.gke.io/gcp-service-account=${GSA_NAME}@${PROJECT_ID}.iam.gserviceaccount.com
kubectl annotate serviceaccount --namespace ${K8S_NAMESPACE} ${KSA_NAME} amazonaws.com/role-arn=${AWS_ROLE_ARN}
Run a new K8s Pod with K8s
${KSA_NAME}
Service Account:# run a pod (with AWS CLI onboard) in interactive mod
kubectl run -it --rm --generator=run-pod/v1 --image mikesir87/aws-cli --serviceaccount ${KSA_NAME} test-pod
# in Pod shell: check AWS assumed role
aws sts get-caller-identity
# the output should look similar to below
{
"UserId": "AROA9GB4GPRFFXVHNSLCK:gtoken-webhook-gyaashbbeeqhpvfw",
"Account": "906385953612",
"Arn": "arn:aws:sts::906385953612:assumed-role/bucket-full-gtoken/gtoken-webhook-gyaashbbeeqhpvfw"
}