Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
7725732
Included changes for python script for custom credential suppliers.
vverman Nov 13, 2025
95418ac
Made some test and format changes.
vverman Nov 14, 2025
6f9727c
Scripts now read from a file instead of env variables. Changed readme…
vverman Nov 22, 2025
4fc575e
Added license header to pod.yaml.
vverman Nov 22, 2025
1956519
fix: Update Dockerfile
iennae Dec 12, 2025
8e72754
fix: clarify comments
iennae Dec 12, 2025
8529f82
fix: refactor main to seprate concerns simplify testing
iennae Dec 12, 2025
f78858b
fix: update testing to match refactored main.
iennae Dec 12, 2025
2953b75
fix: update version to test
iennae Dec 12, 2025
ea809bc
fix: use latest python
iennae Dec 12, 2025
0bd179b
fix: last line
iennae Dec 12, 2025
633fd3a
fix: address issues introduced in gitignore file
iennae Dec 12, 2025
d265bbb
fix: cleanup README documentation.
iennae Dec 12, 2025
24e9b29
fix: refine the README instructions.
iennae Dec 12, 2025
6d361d4
fix: Apply suggestion from @gemini-code-assist[bot]
iennae Dec 12, 2025
b6fb718
fix: starting region tag
iennae Dec 12, 2025
d0ed98d
fix: address whitespace linting issue
iennae Dec 12, 2025
e4b1053
fix: address linting
iennae Dec 12, 2025
e89f6a0
Now using the storage library instead of calling the storage endpoint.
vverman Dec 12, 2025
7f61120
Removed unnecessary comments.
vverman Dec 12, 2025
05d775c
Formatting changes.
vverman Dec 13, 2025
613bf33
Changed default scopes.
vverman Dec 13, 2025
79ee751
Fixed PR Build run fixes.
vverman Dec 13, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,8 @@ env/
.idea
.env*
**/venv
**/noxfile.py
**/noxfile.py

# Auth Local secrets file
auth/custom-credentials/okta/custom-credentials-okta-secrets.json
auth/custom-credentials/aws/custom-credentials-aws-secrets.json
15 changes: 15 additions & 0 deletions auth/custom-credentials/aws/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
FROM python:3.13-slim

RUN useradd -m appuser

WORKDIR /app

COPY --chown=appuser:appuser requirements.txt .

USER appuser
RUN pip install --no-cache-dir -r requirements.txt

COPY --chown=appuser:appuser snippets.py .


CMD ["python3", "snippets.py"]
127 changes: 127 additions & 0 deletions auth/custom-credentials/aws/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
# Running the Custom AWS Credential Supplier Sample

This sample demonstrates how to use a custom AWS security credential supplier to authenticate with Google Cloud using AWS as an external identity provider. It uses Boto3 (the AWS SDK for Python) to fetch credentials from sources like Amazon Elastic Kubernetes Service (EKS) with IAM Roles for Service Accounts(IRSA), Elastic Container Service (ECS), or Fargate.

## Prerequisites

* An AWS account.
* A Google Cloud project with the IAM API enabled.
* A GCS bucket.
* Python 3.10 or later installed.

If you want to use AWS security credentials that cannot be retrieved using methods supported natively by the [google-auth](https://github.com/googleapis/google-auth-library-python) library, a custom `AwsSecurityCredentialsSupplier` implementation may be specified. The supplier must return valid, unexpired AWS security credentials when called by the Google Cloud Auth library.


## Running Locally

For local development, you can provide credentials and configuration in a JSON file.

### Install Dependencies

Ensure you have Python installed, then install the required libraries:

```bash
pip install -r requirements.txt
```

### Configure Credentials for Local Development

1. Copy the example secrets file to a new file named `custom-credentials-aws-secrets.json`:
```bash
cp custom-credentials-aws-secrets.json.example custom-credentials-aws-secrets.json
```
2. Open `custom-credentials-aws-secrets.json` and fill in the required values for your AWS and Google Cloud configuration. Do not check your `custom-credentials-aws-secrets.json` file into version control.

**Note:** This file is only used for local development and is not needed when running in a containerized environment like EKS with IRSA.


### Run the Script

```bash
python3 snippets.py
```

When run locally, the script will detect the `custom-credentials-aws-secrets.json` file and use it to configure the necessary environment variables for the Boto3 client.

## Running in a Containerized Environment (EKS)

This section provides a brief overview of how to run the sample in an Amazon EKS cluster.

### EKS Cluster Setup

First, you need an EKS cluster. You can create one using `eksctl` or the AWS Management Console. For detailed instructions, refer to the [Amazon EKS documentation](https://docs.aws.amazon.com/eks/latest/userguide/create-cluster.html).

### Configure IAM Roles for Service Accounts (IRSA)

IRSA enables you to associate an IAM role with a Kubernetes service account. This provides a secure way for your pods to access AWS services without hardcoding long-lived credentials.

Run the following command to create the IAM role and bind it to a Kubernetes Service Account:

```bash
eksctl create iamserviceaccount \
--name your-k8s-service-account \
--namespace default \
--cluster your-cluster-name \
--region your-aws-region \
--role-name your-role-name \
--attach-policy-arn arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess \
--approve
```

> **Note**: The `--attach-policy-arn` flag is used here to demonstrate attaching permissions. Update this with the specific AWS policy ARN your application requires.

For a deep dive into how this works without using `eksctl`, refer to the [IAM Roles for Service Accounts](https://docs.aws.amazon.com/eks/latest/userguide/iam-roles-for-service-accounts.html) documentation.

### Configure Google Cloud to Trust the AWS Role

To allow your AWS role to authenticate as a Google Cloud service account, you need to configure Workload Identity Federation. This process involves these key steps:

1. **Create a Workload Identity Pool and an AWS Provider:** The pool holds the configuration, and the provider is set up to trust your AWS account.

2. **Create or select a Google Cloud Service Account:** This service account will be impersonated by your AWS role.

3. **Bind the AWS Role to the Google Cloud Service Account:** Create an IAM policy binding that gives your AWS role the `Workload Identity User` (`roles/iam.workloadIdentityUser`) role on the Google Cloud service account.

For more detailed information, see the documentation on [Configuring Workload Identity Federation](https://cloud.google.com/iam/docs/workload-identity-federation-with-other-clouds).

**Alternative: Direct Access**

> For supported resources, you can grant roles directly to the AWS identity, bypassing service account impersonation. To do this, grant a role (like `roles/storage.objectViewer`) to the workload identity principal (`principalSet://...`) directly on the resource's IAM policy.

For more detailed information, see the documentation on [Configuring Workload Identity Federation](https://cloud.google.com/iam/docs/workload-identity-federation-with-other-clouds).

### Containerize and Package the Application

Create a `Dockerfile` for the Python application and push the image to a container registry (for example Amazon ECR) that your EKS cluster can access.

**Note:** The provided [`Dockerfile`](Dockerfile) is an example and may need to be modified for your specific needs.

Build and push the image:
```bash
docker build -t your-container-image:latest .
docker push your-container-image:latest
```

### Deploy to EKS

Create a Kubernetes deployment manifest to deploy your application to the EKS cluster. See the [`pod.yaml`](pod.yaml) file for an example.

**Note:** The provided [`pod.yaml`](pod.yaml) is an example and may need to be modified for your specific needs.

Deploy the pod:

```bash
kubectl apply -f pod.yaml
```

### Clean Up

To clean up the resources, delete the EKS cluster and any other AWS and Google Cloud resources you created.

```bash
eksctl delete cluster --name your-cluster-name
```

## Testing

This sample is not continuously tested. It is provided for instructional purposes and may require modifications to work in your environment.
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"aws_access_key_id": "YOUR_AWS_ACCESS_KEY_ID",
"aws_secret_access_key": "YOUR_AWS_SECRET_ACCESS_KEY",
"aws_region": "YOUR_AWS_REGION",
"gcp_workload_audience": "YOUR_GCP_WORKLOAD_AUDIENCE",
"gcs_bucket_name": "YOUR_GCS_BUCKET_NAME",
"gcp_service_account_impersonation_url": "YOUR_GCP_SERVICE_ACCOUNT_IMPERSONATION_URL"
}
17 changes: 17 additions & 0 deletions auth/custom-credentials/aws/noxfile_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

TEST_CONFIG_OVERRIDE = {
"ignored_versions": ["2.7", "3.6", "3.7", "3.8", "3.9", "3.10", "3.11", "3.12"],
}
40 changes: 40 additions & 0 deletions auth/custom-credentials/aws/pod.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Copyright 2025 Google LLC
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

apiVersion: v1
kind: Pod
metadata:
name: custom-credential-pod
spec:
# The Kubernetes Service Account that is annotated with the corresponding
# AWS IAM role ARN. See the README for instructions on setting up IAM
# Roles for Service Accounts (IRSA).
serviceAccountName: your-k8s-service-account
containers:
- name: gcp-auth-sample
# The container image pushed to the container registry
# For example, Amazon Elastic Container Registry
image: your-container-image:latest
env:
# REQUIRED: The AWS region. Boto3 requires this to be set explicitly
# in containers.
- name: AWS_REGION
value: "your-aws-region"
# REQUIRED: The full identifier of the Workload Identity Pool provider
- name: GCP_WORKLOAD_AUDIENCE
value: "your-gcp-workload-audience"
# OPTIONAL: Enable Google Cloud service account impersonation
# - name: GCP_SERVICE_ACCOUNT_IMPERSONATION_URL
# value: "your-gcp-service-account-impersonation-url"
- name: GCS_BUCKET_NAME
value: "your-gcs-bucket-name"
2 changes: 2 additions & 0 deletions auth/custom-credentials/aws/requirements-test.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-r requirements.txt
pytest==8.2.0
5 changes: 5 additions & 0 deletions auth/custom-credentials/aws/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
boto3==1.40.53
google-auth==2.43.0
google-cloud-storage==2.19.0
python-dotenv==1.1.1
requests==2.32.3
153 changes: 153 additions & 0 deletions auth/custom-credentials/aws/snippets.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
# Copyright 2025 Google LLC
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

# [START auth_custom_credential_supplier_aws]
import json
import os
import sys

import boto3
from google.auth import aws
from google.auth import exceptions
from google.cloud import storage


class CustomAwsSupplier(aws.AwsSecurityCredentialsSupplier):
"""Custom AWS Security Credentials Supplier using Boto3."""

def __init__(self):
"""Initializes the Boto3 session, prioritizing environment variables for region."""
# Explicitly read the region from the environment first.
region = os.getenv("AWS_REGION") or os.getenv("AWS_DEFAULT_REGION")

# If region is None, Boto3's discovery chain will be used when needed.
self.session = boto3.Session(region_name=region)
self._cached_region = None

def get_aws_region(self, context, request) -> str:
"""Returns the AWS region using Boto3's default provider chain."""
if self._cached_region:
return self._cached_region

self._cached_region = self.session.region_name

if not self._cached_region:
raise exceptions.GoogleAuthError(
"Boto3 was unable to resolve an AWS region."
)

return self._cached_region

def get_aws_security_credentials(
self, context, request=None
) -> aws.AwsSecurityCredentials:
"""Retrieves AWS security credentials using Boto3's default provider chain."""
creds = self.session.get_credentials()
if not creds:
raise exceptions.GoogleAuthError(
"Unable to resolve AWS credentials from Boto3."
)

return aws.AwsSecurityCredentials(
access_key_id=creds.access_key,
secret_access_key=creds.secret_key,
session_token=creds.token,
)


def authenticate_with_aws_credentials(bucket_name, audience, impersonation_url=None):
"""Authenticates using the custom AWS supplier and gets bucket metadata.

Returns:
dict: The bucket metadata response from the Google Cloud Storage API.
"""

custom_supplier = CustomAwsSupplier()

credentials = aws.Credentials(
audience=audience,
subject_token_type="urn:ietf:params:aws:token-type:aws4_request",
service_account_impersonation_url=impersonation_url,
aws_security_credentials_supplier=custom_supplier,
scopes=["https://www.googleapis.com/auth/devstorage.read_only"],
)

storage_client = storage.Client(credentials=credentials)

bucket = storage_client.get_bucket(bucket_name)

return bucket._properties


# [END auth_custom_credential_supplier_aws]


def _load_config_from_file():
"""
If a local secrets file is present, load it into the environment.
This is a "just-in-time" configuration for local development. These
variables are only set for the current process and are not exposed to the
shell.
"""
secrets_file = "custom-credentials-aws-secrets.json"
if os.path.exists(secrets_file):
with open(secrets_file, "r") as f:
try:
secrets = json.load(f)
except json.JSONDecodeError:
print(f"Error: '{secrets_file}' is not valid JSON.", file=sys.stderr)
return

os.environ["AWS_ACCESS_KEY_ID"] = secrets.get("aws_access_key_id", "")
os.environ["AWS_SECRET_ACCESS_KEY"] = secrets.get("aws_secret_access_key", "")
os.environ["AWS_REGION"] = secrets.get("aws_region", "")
os.environ["GCP_WORKLOAD_AUDIENCE"] = secrets.get("gcp_workload_audience", "")
os.environ["GCS_BUCKET_NAME"] = secrets.get("gcs_bucket_name", "")
os.environ["GCP_SERVICE_ACCOUNT_IMPERSONATION_URL"] = secrets.get(
"gcp_service_account_impersonation_url", ""
)


def main():

# Reads the custom-credentials-aws-secrets.json if running locally.
_load_config_from_file()

# Now, read the configuration from the environment. In a local run, these
# will be the values we just set. In a containerized run, they will be
# the values provided by the environment.
gcp_audience = os.getenv("GCP_WORKLOAD_AUDIENCE")
sa_impersonation_url = os.getenv("GCP_SERVICE_ACCOUNT_IMPERSONATION_URL")
gcs_bucket_name = os.getenv("GCS_BUCKET_NAME")

if not all([gcp_audience, gcs_bucket_name]):
print(
"Required configuration missing. Please provide it in a "
"custom-credentials-aws-secrets.json file or as environment variables: "
"GCP_WORKLOAD_AUDIENCE, GCS_BUCKET_NAME"
)
return

try:
print(f"Retrieving metadata for bucket: {gcs_bucket_name}...")
metadata = authenticate_with_aws_credentials(
gcs_bucket_name, gcp_audience, sa_impersonation_url
)
print("--- SUCCESS! ---")
print(json.dumps(metadata, indent=2))
except Exception as e:
print(f"Authentication or Request failed: {e}")


if __name__ == "__main__":
main()
Loading