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.
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.
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)
- 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
- Go 1.25+
- (Optional) AWS Account with KMS access for production
- (Optional) Docker & Docker Compose
- Clone the repository:
git clone https://github.com/wolfeidau/ci-oidc-ca
cd ci-oidc-ca- Build the binary:
make build- Run with local signer (development mode):
./bin/ci-oidc-ca \
--signer-type=local \
--port=8080 \
--trust-policies-file=./configs/policies.jsonThis will automatically generate a local CA key and certificate in ./certs/.
- 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"]
}
]
}- Create a KMS key for CA signing:
./scripts/setup.sh- Run with KMS signer:
./bin/ci-oidc-ca \
--signer-type=kms \
--kms-key-id="alias/ci-oidc-ca" \
--trust-policies-file=./configs/policies.json- Or use Docker Compose:
docker-compose up -djobs:
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=.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=.GET /health- Health checkGET /ca-cert- Download CA certificate
-
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" }
The service uses Kong CLI for configuration, supporting both command-line flags and environment variables.
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)
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{
"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"]
}The service embeds OIDC claims as X.509 extensions using these OIDs:
1.3.6.1.4.1.60000.1- OIDC Issuer1.3.6.1.4.1.60000.2- OIDC Subject1.3.6.1.4.1.60000.3- Repository (full path, e.g., "owner/repo")1.3.6.1.4.1.60000.4- Pipeline1.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.
- Key Security: CA private key never leaves AWS KMS (FIPS 140-2 Level 3)
- Short-lived Certificates: 15-minute lifetime eliminates need for revocation
- Zero Trust: Every certificate includes verified OIDC claims
- Audit Trail: All certificate issuance logged with claims
- Network Security: Use TLS for the CA service itself in production
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"]
}
]
}{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"kms:Sign",
"kms:GetPublicKey",
"kms:DescribeKey"
],
"Resource": "arn:aws:kms:region:account:key/*"
}
]
}The service logs all operations in JSON format. Key events:
- Certificate issuance with OIDC claims
- Authentication failures
- Policy violations
- KMS signing operations
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 issuesThe 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.gois just an entry point (39 lines)- All business logic in
internal/serveris fully testable - Public packages (
pkg/) are reusable and well-documented - Clear dependency flow: cmd → internal → pkg
make test # Run all tests with race detection
make lint-fix # Fix linting issues- Start the server with local signer:
./bin/ci-oidc-ca --signer-type=local --port=8080- Check health:
curl http://localhost:8080/health- Get CA certificate:
curl http://localhost:8080/ca-cert -o ca.pem- 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)\"}"MIT
Pull requests welcome! Please include tests for new features.