Skip to content

wolfeidau/ci-oidc-ca

Repository files navigation

CI OIDC CA Service

A Go-based Certificate Authority service that issues short-lived mTLS certificates to CI/CD systems based on OIDC token validation. Supports both AWS KMS for production and local signing for development.

Why mTLS for Service Authentication?

Mutual TLS (mTLS) provides strong, cryptographic authentication for service-to-service communication and is widely adopted in enterprise security architectures:

  • Zero Trust Security: Every connection requires cryptographic proof of identity, eliminating implicit trust
  • Strong Authentication: Private key possession proves identity, far stronger than shared secrets or API keys
  • Encrypted Transport: TLS encryption is built-in, protecting data in transit
  • Auditability: Certificate metadata (OIDC claims as X.509 extensions) provides detailed audit trails
  • Short-lived Credentials: Certificates expire quickly (default 15 minutes), minimizing exposure from compromised credentials
  • No Secret Management: Eliminates need to distribute and rotate long-lived API keys or passwords
  • Compliance: Meets enterprise security requirements for cryptographic authentication (SOC2, PCI-DSS, FedRAMP)

Common use cases:

  • BuildKit: Secure remote Docker build infrastructure
  • Kubernetes: Pod-to-pod authentication and authorization
  • Service Mesh: Istio, Linkerd, Consul Connect
  • Databases: PostgreSQL, MySQL, MongoDB client authentication
  • API Gateways: Kong, NGINX, Envoy upstream authentication
  • Internal APIs: Microservice authentication without service accounts

This service bridges the gap between CI/CD OIDC tokens (temporary, workload-specific) and mTLS certificates, enabling zero-trust authentication for ephemeral build workloads.

Architecture Overview

CI System (GitHub/Buildkite/GitLab)
    ↓ [OIDC Token]
CA Service API
    ├── Validates OIDC token against trust policies
    ├── Issues short-lived certificate (15 min)
    └── Signs with AWS KMS
    ↓ [mTLS Certificate]
BuildKit Daemon (mTLS authenticated)

Features

  • OIDC Authentication: Validates tokens from GitHub Actions, Buildkite, GitLab CI, CircleCI, etc.
  • Dual Signer Support:
    • KMS Signer: CA private key never leaves AWS KMS HSM (FIPS 140-2 Level 3) for production
    • Local Signer: File-based ECDSA P-256 keys for development and testing
  • Short-lived Certificates: Configurable lifetime (default 15 minutes) eliminates need for revocation
  • Trust Policies: Flexible JSON-based policies to control which OIDC tokens are trusted
  • OIDC Claims in Certificates: Embeds claims as X.509 extensions for audit trail
  • Zero Trust: Every build gets unique certificate based on verified identity
  • CLI Configuration: Kong-based CLI with flag and environment variable support

Quick Start

Prerequisites

  • Go 1.25+
  • (Optional) AWS Account with KMS access for production
  • (Optional) Docker & Docker Compose

Local Development Setup

  1. Clone the repository:
git clone https://github.com/wolfeidau/ci-oidc-ca
cd ci-oidc-ca
  1. Build the binary:
make build
  1. Run with local signer (development mode):
./bin/ci-oidc-ca \
  --signer-type=local \
  --port=8080 \
  --trust-policies-file=./configs/policies.json

This will automatically generate a local CA key and certificate in ./certs/.

  1. Configure trust policies in configs/policies.json:
{
  "trust_policies": [
    {
      "name": "GitHub Actions - Main Branch",
      "issuer": "https://token.actions.githubusercontent.com",
      "required_claims": {
        "repository": "your-org/your-repo",
        "ref": "refs/heads/main"
      },
      "permissions": ["build", "push"]
    }
  ]
}

Production Setup with AWS KMS

  1. Create a KMS key for CA signing:
./scripts/setup.sh
  1. Run with KMS signer:
./bin/ci-oidc-ca \
  --signer-type=kms \
  --kms-key-id="alias/ci-oidc-ca" \
  --trust-policies-file=./configs/policies.json
  1. Or use Docker Compose:
docker-compose up -d

Usage

From GitHub Actions

jobs:
  build:
    runs-on: ubuntu-latest
    permissions:
      id-token: write  # Required for OIDC
    steps:
      - name: Get OIDC Token
        uses: actions/github-script@v6
        with:
          script: |
            const token = await core.getIDToken('https://your-ca-service.com')
            core.setSecret(token)
            core.exportVariable('OIDC_TOKEN', token)
      
      - name: Get mTLS Certificate
        run: |
          # Generate CSR
          openssl req -new -newkey rsa:2048 -nodes \
            -keyout client.key -out client.csr \
            -subj "/CN=github-builder/O=CI"
          
          # Request certificate
          curl -X POST https://your-ca-service.com/v1/certificates \
            -H "Authorization: Bearer $OIDC_TOKEN" \
            -H "Content-Type: application/json" \
            -d "{\"csr\": \"$(cat client.csr | base64 -w0)\"}" \
            -o response.json
          
          # Extract certificate
          jq -r '.certificate' response.json > client.crt
          jq -r '.ca_cert' response.json > ca.crt
      
      - name: Build with BuildKit
        run: |
          buildctl \
            --addr tcp://buildkit.example.com:8443 \
            --tlscert client.crt \
            --tlskey client.key \
            --tlscacert ca.crt \
            build \
            --frontend dockerfile.v0 \
            --local context=. \
            --local dockerfile=.

From Buildkite

steps:
  - label: ":docker: Build with mTLS"
    plugins:
      - oidc-checkout#v1:
          audience: https://your-ca-service.com
    commands:
      - |
        # BUILDKITE_OIDC_TOKEN is set by the plugin
        CSR=$(openssl req -new -newkey rsa:2048 -nodes -keyout client.key -subj "/CN=buildkite-builder/O=CI" 2>/dev/null)
        
        RESPONSE=$(curl -X POST https://your-ca-service.com/v1/certificates \
          -H "Authorization: Bearer $BUILDKITE_OIDC_TOKEN" \
          -H "Content-Type: application/json" \
          -d "{\"csr\": \"$CSR\"}")
        
        echo "$RESPONSE" | jq -r '.certificate' > client.crt
        echo "$RESPONSE" | jq -r '.ca_cert' > ca.crt
        
        # Use with buildctl
        buildctl --addr tcp://buildkit:8443 \
          --tlscert client.crt --tlskey client.key --tlscacert ca.crt \
          build --frontend dockerfile.v0 --local context=. --local dockerfile=.

API Endpoints

Public Endpoints

  • GET /health - Health check
  • GET /ca-cert - Download CA certificate

Protected Endpoints (require OIDC token)

  • POST /v1/certificates - Issue a new certificate

    {
      "csr": "-----BEGIN CERTIFICATE REQUEST-----..."
    }

    Response:

    {
      "certificate": "-----BEGIN CERTIFICATE-----...",
      "ca_cert": "-----BEGIN CERTIFICATE-----...",
      "expires_at": "2024-01-01T12:15:00Z"
    }

Configuration

The service uses Kong CLI for configuration, supporting both command-line flags and environment variables.

Command-Line Flags

Run ./bin/ci-oidc-ca --help to see all available options:

--port=8080                           Port to listen on ($PORT)
--cert-lifetime=15m                   Certificate lifetime ($CERT_LIFETIME)
--trust-policies-file=./trust-policies.yaml
                                      Path to trust policies file ($TRUST_POLICIES_FILE)
--signer-type=kms                     Signer type: 'kms' or 'local' ($SIGNER_TYPE)
--kms-key-id=""                       AWS KMS key ID ($KMS_KEY_ID)
--ca-cert-file=./certs/ca-cert.pem    CA cert file ($CA_CERT_FILE)
--local-key-path=./certs/ca-key.pem   Local key path ($LOCAL_KEY_PATH)
--local-cert-path=./certs/ca-cert.pem Local cert path ($LOCAL_CERT_PATH)

Environment Variables

All flags can be set via environment variables (shown in parentheses above):

export PORT=8080
export SIGNER_TYPE=local
export TRUST_POLICIES_FILE=./configs/policies.json
./bin/ci-oidc-ca

Trust Policy Structure

{
  "name": "Policy name",
  "issuer": "https://token.issuer.url",
  "audience": ["optional", "audience", "values"],
  "required_claims": {
    "claim_name": "exact_value",
    "other_claim": ["one_of", "these_values"],
    "wildcard_claim": "prefix*"
  },
  "permissions": ["build", "push", "deploy"]
}

Certificate Extensions

The service embeds OIDC claims as X.509 extensions using these OIDs:

  • 1.3.6.1.4.1.60000.1 - OIDC Issuer
  • 1.3.6.1.4.1.60000.2 - OIDC Subject
  • 1.3.6.1.4.1.60000.3 - Repository (full path, e.g., "owner/repo")
  • 1.3.6.1.4.1.60000.4 - Pipeline
  • 1.3.6.1.4.1.60000.5 - Repository Owner (organization/owner)

The repository owner is also embedded in the certificate's Organization field for multi-tenant support.

Security Considerations

  1. Key Security: CA private key never leaves AWS KMS (FIPS 140-2 Level 3)
  2. Short-lived Certificates: 15-minute lifetime eliminates need for revocation
  3. Zero Trust: Every certificate includes verified OIDC claims
  4. Audit Trail: All certificate issuance logged with claims
  5. Network Security: Use TLS for the CA service itself in production

Production Deployment

Kubernetes

apiVersion: apps/v1
kind: Deployment
metadata:
  name: ci-oidc-ca
spec:
  replicas: 2
  template:
    metadata:
      labels:
        app: ci-oidc-ca
    spec:
      serviceAccountName: ci-oidc-ca
      containers:
      - name: ca
        image: your-registry/ci-oidc-ca:latest
        args:
          - --signer-type=kms
          - --kms-key-id=alias/ci-oidc-ca
          - --trust-policies-file=/etc/policies/policies.json
          - --port=8080
        ports:
        - containerPort: 8080
          name: http
        livenessProbe:
          httpGet:
            path: /health
            port: 8080
          initialDelaySeconds: 5
          periodSeconds: 10
        readinessProbe:
          httpGet:
            path: /health
            port: 8080
          initialDelaySeconds: 3
          periodSeconds: 5
        volumeMounts:
        - name: policies
          mountPath: /etc/policies
          readOnly: true
      volumes:
      - name: policies
        configMap:
          name: trust-policies
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: trust-policies
data:
  policies.json: |
    {
      "trust_policies": [
        {
          "name": "GitHub Actions - Production",
          "issuer": "https://token.actions.githubusercontent.com",
          "audience": ["https://github.com/your-org"],
          "required_claims": {
            "repository": "your-org/your-repo",
            "ref": "refs/heads/main"
          },
          "permissions": ["build", "push"]
        }
      ]
    }

AWS IAM Policy

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "kms:Sign",
        "kms:GetPublicKey",
        "kms:DescribeKey"
      ],
      "Resource": "arn:aws:kms:region:account:key/*"
    }
  ]
}

Monitoring

The service logs all operations in JSON format. Key events:

  • Certificate issuance with OIDC claims
  • Authentication failures
  • Policy violations
  • KMS signing operations

Development

Building

Use the Makefile for all build operations:

make help        # Show all available targets
make build       # Build the binary
make test        # Run tests with race detection and coverage
make lint-fix    # Run linters and auto-fix issues

Project Structure

The codebase follows clean architecture principles with clear separation of concerns:

ci-oidc-ca/
├── cmd/server/              # Application entry point (39 lines)
│   └── main.go              # CLI setup with Kong, minimal logic
├── internal/server/         # Business logic (unexported, testable)
│   ├── server.go            # HTTP server orchestration
│   ├── config.go            # Configuration from CLI params
│   ├── handlers.go          # HTTP request handlers
│   ├── middleware.go        # Logging and recovery middleware
│   ├── signer.go            # Signer initialization
│   └── types.go             # Request/response types
├── pkg/                     # Reusable public packages
│   ├── auth/                # OIDC token validation
│   │   └── oidc.go          # Provider setup, token verification
│   ├── ca/                  # Certificate authority operations
│   │   └── ca.go            # Certificate issuance, claim embedding
│   └── signer/              # Signing implementations
│       ├── kms.go           # AWS KMS signer
│       ├── local.go         # Local file-based signer
│       └── local_test.go    # Comprehensive local signer tests
├── configs/                 # Configuration files
│   └── policies.json        # Example trust policies
├── specs/                   # Documentation
│   ├── e2e_testing.md       # End-to-end testing plan
│   └── initial_review.md    # Initial project review
└── Makefile                 # Build, test, lint targets

Architecture benefits:

  • cmd/server/main.go is just an entry point (39 lines)
  • All business logic in internal/server is fully testable
  • Public packages (pkg/) are reusable and well-documented
  • Clear dependency flow: cmd → internal → pkg

Running Tests

make test        # Run all tests with race detection
make lint-fix    # Fix linting issues

Testing Locally

  1. Start the server with local signer:
./bin/ci-oidc-ca --signer-type=local --port=8080
  1. Check health:
curl http://localhost:8080/health
  1. Get CA certificate:
curl http://localhost:8080/ca-cert -o ca.pem
  1. Request a certificate (requires valid OIDC token from your CI):
# Generate test CSR
openssl req -new -newkey ecdsa -pkeyopt ec_paramgen_curve:P-256 -nodes \
  -keyout test.key -out test.csr \
  -subj "/CN=test/O=Test"

# Get OIDC token from your CI system
TOKEN="your-oidc-token-here"

# Request certificate
curl -X POST http://localhost:8080/v1/certificates \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d "{\"csr\": \"$(cat test.csr | base64)\"}"

License

MIT

Contributing

Pull requests welcome! Please include tests for new features.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors