Organizational

CodeArtifact + GitHub Actions

Pattern for secure private package management using AWS CodeArtifact with OIDC authentication.

Production

Problem

Teams need to securely consume private packages and publish internal libraries from GitHub Actions workflows without managing AWS credentials as secrets, while following least-privilege principles and maintaining separation between read and publish operations.

Context

When to Use This Pattern

  • Publishing internal npm or Python packages from CI/CD pipelines
  • Installing private dependencies in application builds
  • Implementing package registries with upstream caching to public registries (npmjs, PyPI)
  • Requiring audit trails for package operations
  • Working with GitHub-hosted runners in organizational repositories

When NOT to Use This Pattern

  • Simple projects with only public dependencies (use public registries directly)
  • Self-hosted runners with IAM instance profiles (use instance profiles instead)
  • Projects requiring Maven/NuGet (adapt the pattern for those package managers)
  • Local development environments (use AWS CLI aws codeartifact login with personal credentials)

Solution

Use GitHub OIDC (OpenID Connect) to authenticate GitHub Actions workflows to AWS without storing long-lived credentials. Configure separate IAM roles for read-only (CI builds) and publish (releases) operations, with trust policies that restrict access based on repository, branch, or tag patterns.

Core Components

  1. CodeArtifact Infrastructure: Domain and repositories managed in the central infra repository
  2. IAM Policies: Separate read and write policies with least-privilege permissions
  3. IAM Roles: OIDC-based roles with trust policies restricting GitHub repository access
  4. GitHub Workflows: Thin wrappers around Taskfile tasks that assume AWS roles via OIDC
  5. Taskfile Tasks: Reusable package operations (install, publish) that can run locally and in CI

Structure

Infrastructure Layout

infra/
└── global/
    └── codeartifact/
        ├── main.tf           # Domain and repositories
        ├── iam.tf            # Policies and roles
        ├── outputs.tf        # Outputs for consumption
        └── variables.tf      # Configuration variables

Current Ontopix Configuration

Based on infra/global/codeartifact/:

  • Domain: ontopix
  • Region: eu-central-1
  • Repositories:
    • npm (internal) with npm-upstream (proxy to public npm)
    • pypi (internal) with pypi-upstream (proxy to public PyPI)
  • IAM Policies:
    • CodeArtifactReadAccess: Read-only operations
    • CodeArtifactWriteAccess: Read and publish operations

Implementation

Step 1: Configure GitHub OIDC Provider (Infrastructure)

The current infrastructure needs to be updated to support GitHub Actions OIDC. This requires:

1.1: Create OIDC Provider (if not exists)

# infra/global/iam/github_oidc.tf
resource "aws_iam_openid_connect_provider" "github_actions" {
  url = "https://token.actions.githubusercontent.com"

  client_id_list = [
    "sts.amazonaws.com"
  ]

  thumbprint_list = [
    "6938fd4d98bab03faadb97b34396831e3780aea1"  # GitHub OIDC thumbprint
  ]

  tags = {
    Name        = "GitHubActionsOIDC"
    Environment = "global"
    ManagedBy   = "terraform"
  }
}

1.2: Update CodeArtifact IAM Roles with OIDC Trust Policy

Replace the current CodeArtifactCICDRole with two separate roles:

# infra/global/codeartifact/iam.tf

# Read-only role for CI (tests, builds, PR checks)
resource "aws_iam_role" "codeartifact_github_read" {
  name        = "GitHubActions-CodeArtifact-ReadRole"
  description = "Read-only access to CodeArtifact for GitHub Actions CI workflows"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect = "Allow"
        Principal = {
          Federated = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:oidc-provider/token.actions.githubusercontent.com"
        }
        Action = "sts:AssumeRoleWithWebIdentity"
        Condition = {
          StringEquals = {
            "token.actions.githubusercontent.com:aud" = "sts.amazonaws.com"
          }
          StringLike = {
            # Allow any repository in the ontopix organization
            "token.actions.githubusercontent.com:sub" = "repo:ontopix/*:*"
          }
        }
      }
    ]
  })

  tags = {
    Name        = "GitHubActions-CodeArtifact-ReadRole"
    Environment = "global"
    ManagedBy   = "terraform"
  }
}

resource "aws_iam_role_policy_attachment" "github_read_access" {
  role       = aws_iam_role.codeartifact_github_read.name
  policy_arn = aws_iam_policy.codeartifact_read.arn
}

# Publish role for releases (restricted to tags)
resource "aws_iam_role" "codeartifact_github_publish" {
  name        = "GitHubActions-CodeArtifact-PublishRole"
  description = "Read and publish access to CodeArtifact for GitHub Actions release workflows"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect = "Allow"
        Principal = {
          Federated = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:oidc-provider/token.actions.githubusercontent.com"
        }
        Action = "sts:AssumeRoleWithWebIdentity"
        Condition = {
          StringEquals = {
            "token.actions.githubusercontent.com:aud" = "sts.amazonaws.com"
          }
          StringLike = {
            # Only allow releases from version tags
            "token.actions.githubusercontent.com:sub" = "repo:ontopix/*:ref:refs/tags/v*"
          }
        }
      }
    ]
  })

  tags = {
    Name        = "GitHubActions-CodeArtifact-PublishRole"
    Environment = "global"
    ManagedBy   = "terraform"
  }
}

resource "aws_iam_role_policy_attachment" "github_publish_access" {
  role       = aws_iam_role.codeartifact_github_publish.name
  policy_arn = aws_iam_policy.codeartifact_write.arn
}

1.3: Add Outputs for GitHub Actions

# infra/global/codeartifact/outputs.tf (add these)

output "github_read_role_arn" {
  description = "IAM role ARN for GitHub Actions read access"
  value       = aws_iam_role.codeartifact_github_read.arn
}

output "github_publish_role_arn" {
  description = "IAM role ARN for GitHub Actions publish access"
  value       = aws_iam_role.codeartifact_github_publish.arn
}

1.4: Apply Infrastructure Changes

cd ../infra
task infra:plan   # Review changes
# After review and approval
task infra:apply  # Apply changes

Step 2: Configure Repository Taskfile

Add CodeArtifact tasks to your repository's Taskfile.yaml:

# Taskfile.yaml
version: '3'

vars:
  CODEARTIFACT_DOMAIN: ontopix
  CODEARTIFACT_REGION: eu-central-1
  AWS_ACCOUNT_ID:
    sh: aws sts get-caller-identity --query Account --output text 2>/dev/null || echo "unknown"

tasks:
  deps:install:
    desc: Install dependencies from CodeArtifact
    cmds:
      - task: deps:login
      - npm ci  # or pip install -r requirements.txt

  deps:login:
    desc: Login to CodeArtifact (requires AWS credentials)
    cmds:
      - |
        aws codeartifact login \
          --tool npm \
          --domain {{.CODEARTIFACT_DOMAIN}} \
          --domain-owner {{.AWS_ACCOUNT_ID}} \
          --repository npm \
          --region {{.CODEARTIFACT_REGION}}
    preconditions:
      - sh: command -v aws
        msg: "AWS CLI is required"

  deps:login:pypi:
    desc: Login to CodeArtifact for Python (requires AWS credentials)
    cmds:
      - |
        aws codeartifact login \
          --tool pip \
          --domain {{.CODEARTIFACT_DOMAIN}} \
          --domain-owner {{.AWS_ACCOUNT_ID}} \
          --repository pypi \
          --region {{.CODEARTIFACT_REGION}}
    preconditions:
      - sh: command -v aws
        msg: "AWS CLI is required"

  release:publish:
    desc: Publish package to CodeArtifact
    cmds:
      - task: deps:login
      - npm publish  # or python -m build && twine upload
    preconditions:
      - sh: test -f package.json  # or setup.py/pyproject.toml
        msg: "Package configuration not found"

Step 3: Create GitHub Actions Workflows

3.1: CI Workflow (Read-only access)

# .github/workflows/ci.yaml
name: CI

on:
  push:
    branches: [master, main]
  pull_request:
    branches: [master, main]

# OIDC permissions required
permissions:
  id-token: write
  contents: read

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      # Configure AWS credentials via OIDC
      - name: Configure AWS Credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/GitHubActions-CodeArtifact-ReadRole
          aws-region: eu-central-1

      # Setup Task
      - name: Setup Task
        uses: arduino/setup-task@v2

      # Setup language environment
      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'

      # Use Taskfile for operations
      - name: Install Dependencies
        run: task deps:install

      - name: Run Tests
        run: task test:all

      - name: Run Linter
        run: task lint:check

3.2: Release Workflow (Publish access)

# .github/workflows/release.yaml
name: Release

on:
  push:
    tags:
      - 'v*'

# OIDC permissions required
permissions:
  id-token: write
  contents: read

jobs:
  publish:
    runs-on: ubuntu-latest
    # Optional: Use GitHub Environments for additional approval gates
    environment: production

    steps:
      - uses: actions/checkout@v4

      # Configure AWS credentials via OIDC with publish role
      - name: Configure AWS Credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/GitHubActions-CodeArtifact-PublishRole
          aws-region: eu-central-1

      # Setup Task
      - name: Setup Task
        uses: arduino/setup-task@v2

      # Setup language environment
      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'

      # Install, build, and publish via Taskfile
      - name: Install Dependencies
        run: task deps:install

      - name: Build Package
        run: task build

      - name: Publish to CodeArtifact
        run: task release:publish

Step 4: Configure GitHub Repository Secrets

Add the AWS account ID as a GitHub repository secret:

  1. Go to repository SettingsSecrets and variablesActions
  2. Add secret:
    • Name: AWS_ACCOUNT_ID
    • Value: Your AWS account ID (from infra/global/outputs.tf)

Note: This is the ONLY secret required. No AWS access keys needed.

Step 5: Local Development Setup

For local development, developers should authenticate using their personal AWS credentials:

# Configure AWS CLI with personal credentials (one-time setup)
aws configure --profile ontopix

# Login to CodeArtifact
task deps:login

# Install dependencies
task deps:install

# Work normally
task test:all

Applies Principles

  • Security by Design: OIDC removes long-lived credentials, reducing attack surface
  • Least Privilege: Separate roles for read/publish with minimal required permissions
  • Reproducibility: Same Taskfile tasks work locally and in CI
  • Separation of Concerns: GitHub Actions handles triggers/environment, Taskfile handles operations
  • Vendor Neutrality: Logic in Taskfile is portable across CI systems
  • Audit Trail: CloudTrail logs all CodeArtifact operations with GitHub actor identity

Consequences

Benefits

No Credentials in GitHub Secrets: OIDC eliminates long-lived AWS access keys ✅ Automatic Credential Rotation: OIDC tokens expire after use ✅ Fine-Grained Access Control: Trust policies can restrict by repo, branch, tag, or environment ✅ Audit Trail: CloudTrail captures GitHub user and workflow information ✅ Local/CI Parity: Same Taskfile tasks work in both environments ✅ Upstream Caching: Internal repositories cache public packages, improving build reliability ✅ Cost Efficiency: Pay only for private package storage, not bandwidth for cached public packages

Trade-offs

⚠️ Infrastructure Dependency: Requires OIDC provider and IAM role configuration ⚠️ Initial Setup Complexity: More complex than simple access keys (but more secure) ⚠️ GitHub-Specific: OIDC trust policy is tied to GitHub Actions (portable to other OIDC providers with adjustments) ⚠️ Region Dependency: CodeArtifact is regional (Ontopix uses eu-central-1) ⚠️ Learning Curve: Team needs to understand OIDC authentication flow

Limitations

Self-Hosted Runners: If using self-hosted runners, consider instance profiles instead ❌ Cross-Account Access: Pattern needs adjustment for multi-account setups ❌ Non-GitHub CI: Different OIDC configuration required for GitLab, CircleCI, etc.

Examples

Example: npm Package Publishing

# package.json
{
  "name": "@ontopix/my-package",
  "version": "1.0.0",
  "publishConfig": {
    "registry": "https://ontopix-${AWS_ACCOUNT_ID}.d.codeartifact.eu-central-1.amazonaws.com/npm/npm/"
  }
}
# Taskfile.yaml
release:publish:
  desc: Publish npm package to CodeArtifact
  cmds:
    - task: deps:login
    - npm run build
    - npm publish

Example: Python Package Publishing

# pyproject.toml
[project]
name = "ontopix-my-package"
version = "1.0.0"

[tool.poetry]
[[tool.poetry.source]]
name = "ontopix"
url = "https://ontopix-${AWS_ACCOUNT_ID}.d.codeartifact.eu-central-1.amazonaws.com/pypi/pypi/simple/"
# Taskfile.yaml
release:publish:
  desc: Publish Python package to CodeArtifact
  cmds:
    - task: deps:login:pypi
    - python -m build
    - twine upload --repository ontopix dist/*

Variations

Variation 1: Per-Repository Role Restriction

For sensitive repositories, create dedicated roles with repository-specific trust policies:

resource "aws_iam_role" "codeartifact_sensitive_repo" {
  name = "GitHubActions-CodeArtifact-SensitiveRepo"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Effect = "Allow"
      Principal = {
        Federated = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:oidc-provider/token.actions.githubusercontent.com"
      }
      Action = "sts:AssumeRoleWithWebIdentity"
      Condition = {
        StringEquals = {
          "token.actions.githubusercontent.com:aud" = "sts.amazonaws.com"
          # Exact repository match
          "token.actions.githubusercontent.com:sub" = "repo:ontopix/sensitive-repo:ref:refs/tags/v*"
        }
      }
    }]
  })
}

Variation 2: GitHub Environments with Required Approvals

Add manual approval gates for production releases:

# .github/workflows/release.yaml
jobs:
  publish:
    runs-on: ubuntu-latest
    environment:
      name: production
      url: https://npm.ontopix.com/@ontopix/my-package
    # GitHub will require environment approval before running

Configure in GitHub: SettingsEnvironmentsproductionRequired reviewers

Variation 3: Artifact Retention Policies

Configure lifecycle policies in CodeArtifact to manage storage costs:

# infra/global/codeartifact/main.tf
resource "aws_codeartifact_repository" "npm" {
  # ... existing configuration ...

  # Retention policy (requires AWS CLI or API)
  provisioner "local-exec" {
    command = <<-EOT
      aws codeartifact put-package-origin-configuration \
        --domain ${self.domain} \
        --repository ${self.repository} \
        --format npm \
        --package-retention-policy '{"versionRetention": {"type": "COUNT", "value": 10}}'
    EOT
  }
}

References

Ontopix Resources

  • Central infrastructure repository: ../infra/global/codeartifact/
  • IAM policies: CodeArtifactReadAccess, CodeArtifactWriteAccess
  • Domain: ontopix (eu-central-1)

External Documentation

Security Best Practices


Status: ✅ Ready for Implementation Last Updated: 2026-01-28 Infrastructure Status: Requires OIDC provider and role updates in infra/global/