Pulumi Component to setup an artifact registry repository, an OIDC identity provider for Github Actions, and the IAM required to login and push docker images to the registry.
Favors Direct Workload Identity Federation for Github Actions, but supports Workload Identity Federation through a Service Account (CREATE_SERVICE_ACCOUNT=true
) for cases when a GSA is required. Both approaches avoid long-lived access credentials. E.g.:
- name: Authenticate to Google Cloud
uses: google-github-actions/auth@v2
with:
project_id: ${{ env.GCP_PROJECT }}
workload_identity_provider: ${{ env.WORKLOAD_IDENTITY_PROVIDER }}
See:
- https://github.com/google-github-actions/auth/blob/v2.1.10/README.md#setup
- google-github-actions/auth#348
- docker/login-action#640
- https://github.com/docker/login-action?tab=readme-ov-file#google-artifact-registry-gar
-
Artifact Registry Repository
- Docker image storage for CI/CD builds
- Configured with appropriate IAM permissions
- Region-specific or multi-region deployment
-
Workload Identity Federation
- OIDC-based authentication for GitHub Actions
- Secure token exchange without long-lived credentials
- Attribute mapping for repository and actor-based access control
-
IAM Integration
- Automatic permission assignment for Artifact Registry access
- Optional service account and binding to workload identity pool
- Configurable role assignments
go get github.com/davidmontoyago/pulumi-gcp-github-registry
package main
import (
"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
"github.com/davidmontoyago/pulumi-gcp-github-registry/deploy/ci"
)
func main() {
pulumi.Run(func(ctx *pulumi.Context) error {
// Load configuration from environment variables
config, err := ci.LoadConfig()
if err != nil {
return err
}
// Create CI/CD infrastructure
ciInfra, err := ci.NewGithubGoogleRegistryStack(ctx, config)
if err != nil {
return err
}
// Export outputs for GitHub Actions
ctx.Export("registryURL", ciInfra.RegistryURL)
ctx.Export("workloadIdentityProviderID", pulumi.ToSecret(ciInfra.OidcProvider.ID()))
ctx.Export("repositoryWorkloadID", ciInfra.RepositoryPrincipalID)
return nil
})
}
The component uses environment variables for configuration:
Variable | Description | Required | Default |
---|---|---|---|
GCP_PROJECT |
GCP Project ID | Yes | - |
GCP_REGION |
GCP Region for resources | Yes | - |
REPOSITORY_LOCATION |
Artifact Registry location | No | Value of GCP_REGION |
ALLOWED_REPO_URL |
GitHub repository URL for workload identity access | No | https://github.com/davidmontoyago/pulumi-gcp-github-registry |
REPOSITORY_OWNER |
GitHub repository owner (username/org) for additional security | No | - |
REPOSITORY_OWNER_ID |
GitHub repository owner numeric ID (recommended for security) | No | - |
REPOSITORY_ID |
GitHub repository numeric ID (recommended for security) | No | - |
IDENTITY_POOL_PROVIDER_NAME |
Workload identity pool provider name (max 32 chars) | No | github-actions-provider |
RESOURCE_PREFIX |
Prefix for resource names | No | ci |
REPOSITORY_NAME |
Artifact Registry repository name | No | registry |
CREATE_SERVICE_ACCOUNT |
Whether to create a GitHub Actions service account | No | false |
- Configure GitHub Actions Secrets
Add the following secrets to your GitHub repository:
# .github/workflows/deploy.yml
env:
GCP_PROJECT: ${{ secrets.GCP_PROJECT }}
WORKLOAD_IDENTITY_PROVIDER: ${{ secrets.WORKLOAD_IDENTITY_PROVIDER }}
- Authenticate with GCP
Use the google-github-actions/auth
action to authenticate:
- name: Google Auth
id: auth
uses: google-github-actions/auth@v2
with:
project_id: ${{ env.GCP_PROJECT }}
workload_identity_provider: ${{ env.WORKLOAD_IDENTITY_PROVIDER }}
- name: Login to Google Artifact Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY_URL}}
username: oauth2accesstoken
password: ${{ steps.auth.outputs.auth_token }}
- name: Build Docker image
shell: bash
run: make image
name: Deploy to GCP
on:
push:
branches: [main]
pull_request:
branches: [main]
env:
GCP_PROJECT: ${{ secrets.GCP_PROJECT }}
WORKLOAD_IDENTITY_PROVIDER: ${{ secrets.WORKLOAD_IDENTITY_PROVIDER }}
REGISTRY_URL: ${{ secrets.REGISTRY_URL }}
jobs:
deploy:
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
steps:
- uses: actions/checkout@v4
- name: Google Auth
id: auth
uses: google-github-actions/auth@v2
with:
project_id: ${{ env.GCP_PROJECT }}
workload_identity_provider: ${{ env.WORKLOAD_IDENTITY_PROVIDER }}
# optionally get setup for gcloud commands
- name: Set up Cloud SDK
uses: google-github-actions/setup-gcloud@v2
- name: Login to Google Artifact Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY_URL }}
username: oauth2accesstoken
password: ${{ steps.auth.outputs.auth_token }}
- name: Build and Push Image
run: |
docker build -t ${{ env.REGISTRY_URL }}/app:${{ github.sha }} .
docker push ${{ env.REGISTRY_URL }}/app:${{ github.sha }}
- Workload Identity Federation: Eliminates the need for long-lived service account keys
- Least Privilege Access: Service account has minimal required permissions
- Repository Scoping: OIDC provider can be configured to restrict access to specific repositories
- Repository Owner Constraints: Additional security through owner username and numeric ID validation
- Audit Logging: All operations are logged in GCP Cloud Audit Logs
This component implements security best practices for Workload Identity Federation by restricting OIDC authentication to specific GitHub repositories.
See:
The OIDC provider is configured with multiple security layers:
AttributeCondition: pulumi.String("attribute.repository == \"my-org/my-repo\""),
This ensures that only the specified repository can authenticate with the workload identity pool. Any attempt from other repositories will be rejected.
The token audience claim will be validated in GCP against the full name of the OIDC pool provider.
See:
The provider maps GitHub Actions context to GCP attributes for fine-grained control:
GitHub Attribute | GCP Attribute | Description |
---|---|---|
assertion.sub |
google.subject |
Unique identifier for the workflow run |
assertion.actor |
attribute.actor |
GitHub username of the actor |
assertion.repository |
attribute.repository |
Repository name (e.g., owner/repo ) |
assertion.repository_owner |
attribute.repository_owner |
Repository owner (username/org) |
assertion.repository_owner_id |
attribute.repository_owner_id |
Repository owner numeric ID |
assertion.repository_id |
attribute.repository_id |
Repository numeric ID |
assertion.ref |
attribute.ref |
Branch or tag reference |
assertion.sha |
attribute.sha |
Commit SHA |
assertion.workflow |
attribute.workflow |
Workflow name |
assertion.head_ref |
attribute.head_ref |
PR head reference |
assertion.base_ref |
attribute.base_ref |
PR base reference |
- Repository Isolation: Prevents cross-repository access
- Audit Trail: All authentication attempts are logged with repository context
- No Credential Exposure: Eliminates the risk of leaked service account keys
- Automatic Rotation: GitHub Actions tokens are automatically rotated
- Least Privilege: Service account only has necessary permissions
You can verify the repository scoping is working by:
- Checking GCP Cloud Audit Logs for authentication events
- Testing from unauthorized repositories (should be rejected)
- Monitoring the
google.subject
attribute in logs to ensure proper mapping
The component exports the following values for use in CI/CD pipelines:
registryURL
: The full URL of the Artifact Registry repositoryserviceAccountEmail
: The email of the GitHub Actions service accountworkloadIdentityPoolID
: The ID of the workload identity pool (marked as secret)workloadIdentityProviderID
: The full provider ID for GitHub Actions authentication (marked as secret)workloadIdentityProviderCondition
: The attribute condition used for repository scoping
The workloadIdentityPoolId
and workloadIdentityProviderId
are marked as secrets in Pulumi state:
- Encrypted in state: These values are encrypted when stored in Pulumi state files
- Masked in logs: Values are displayed as
[secret]
in Pulumi CLI output - Secure retrieval: Use
pulumi stack output --show-secrets
to view the actual values
Example output:
$ pulumi stack output
Current stack outputs (1):
OUTPUTS
workloadIdentityPoolID [secret]
workloadIdentityProviderID [secret]
registryURL us-docker.pkg.dev/my-project/registry
$ pulumi stack output --show-secrets
Current stack outputs (1):
OUTPUTS
workloadIdentityPoolID projects/123456789/locations/global/workloadIdentityPools/ci-github-actions-pool
workloadIdentityProviderID projects/123456789/locations/global/workloadIdentityPools/ci-github-actions-pool/providers/ci-github-actions-provider
registryURL us-docker.pkg.dev/my-project/registry