Skip to content

nikogura/imgctl

Repository files navigation

imgctl

Container Image Promotion Tool with OIDC Authentication

imgctl is a secure, gRPC-based tool for promoting container images between environments (dev → prod/int) with proper authentication and authorization via OIDC tokens.

Features

  • gRPC API: Fast, type-safe binary protocol with built-in code generation
  • OIDC Authentication: SSH-signed JWT exchanged with Dex for OIDC tokens
  • Group-Based Authorization: Fine-grained access control via group membership
  • Container Image Promotion: Copy images between AWS ECR registries using crane
  • Audit Logging: Comprehensive logging of all operations with user attribution
  • Slack Integration: Optional notifications for image promotions

Architecture

imgctl consists of two main components:

  1. Server: gRPC server that handles authenticated image promotion requests
  2. Client: CLI tool for interacting with the server

Authentication Flow

┌─────────┐      ┌──────────┐      ┌──────────┐      ┌─────────────┐
│  User   │─────▶│ SSH Agent│─────▶│   Dex    │─────▶│ imgctl      │
│  (CLI)  │      │   (JWT)  │      │(OIDC Token)     │  (gRPC)     │
└─────────┘      └──────────┘      └──────────┘      └─────────────┘
  1. Client creates SSH-signed JWT using user's SSH key
  2. JWT is exchanged with Dex for an OIDC token (with proper audience)
  3. OIDC token is sent to imgctl server via gRPC metadata
  4. Server validates token signature, issuer, audience, and group membership
  5. Upon successful auth, server executes the requested operation

Installation

go install github.com/nikogura/imgctl@latest

Or build from source:

git clone https://github.com/nikogura/imgctl
cd imgctl
go build -o imgctl .

Usage

Server

Start the gRPC server:

export OIDC_ISSUER_URL="https://dex.example.com"
export OIDC_AUDIENCE="https://imgctl.example.com"
export OIDC_ALLOWED_GROUPS="engineering@example.com,ops@example.com"

imgctl server --bind-address 0.0.0.0:9999

Server Environment Variables:

Variable Required Purpose
OIDC_ISSUER_URL Yes Dex issuer URL
OIDC_AUDIENCE Yes Expected audience in OIDC tokens (this server's URL)
OIDC_ALLOWED_GROUPS No Comma-separated allowed groups (defaults to engineering@example.com)
IMGCTL_REGISTRY_<NAME> Yes Container registry URL for each named environment
IMGCTL_SPA_<NAME> If using SPA S3 bucket suffix for each named environment
IMGCTL_ECR_REGION No AWS region for ECR login (defaults to us-east-1)
IMGCTL_DEFAULT_SOURCE No Fallback source when client doesn't specify one
IMGCTL_SLACK_WEBHOOK_URL No Slack webhook for promotion notifications

Client

Promote a Container Image

imgctl promote myapp:v1.0.0 --source dev --destination prod

If compile-time defaults are baked in (see Building with Compile-Time Defaults), this simplifies to:

imgctl promote myapp:v1.0.0

Promote SPA / CDN Assets

For static assets deployed via CDN (single-page applications, static sites, etc.), imgctl promotes versioned S3 paths between environment buckets using aws s3 sync:

imgctl promote spa:my-dashboard:v0.1.42 --source dev --destination prod

The spa: prefix tells imgctl to treat the ref as an S3 asset rather than a container image. The format is spa:<name>:<version>. Source and destination map to S3 bucket suffixes configured via IMGCTL_SPA_<NAME> on the server. For example, with IMGCTL_SPA_DEV=dev-01 and IMGCTL_SPA_PROD=prod-01:

  • Source: s3://my-dashboard-dev-01/v0.1.42/
  • Destination: s3://my-dashboard-prod-01/v0.1.42/

Client Flags:

  • --server, -s - gRPC server address (default: IMGCTL_SERVER env var or localhost:9999)
  • --source - Source environment name
  • --destination, -d - Destination environment name
  • --ref, -r - Artifact ref to promote (can also be first positional arg)
  • --dex-url - Dex issuer URL (or DEX_URL env var)
  • --client-id - OAuth2 client ID (or IMGCTL_CLIENT_ID env var)
  • --client-secret - OAuth2 client secret (or IMGCTL_CLIENT_SECRET env var)
  • --pubkey-file, -f - SSH public key file (default: ~/.ssh/id_ed25519.pub)
  • --username, -u - Username for authentication (or KUBECTL_SSH_USER env var)
  • --show-token - Display the OIDC token (for debugging)

Client Environment Variables:

Variable Purpose
IMGCTL_SERVER Default gRPC server address
DEX_URL Dex issuer URL for OIDC authentication
IMGCTL_CLIENT_ID OAuth2 client ID
IMGCTL_CLIENT_SECRET OAuth2 client secret
KUBECTL_SSH_USER Username for SSH authentication
IMGCTL_DEFAULT_SOURCE Default --source value
IMGCTL_DEFAULT_DESTINATION Default --destination value

Check Authentication

Test your OIDC authentication without performing any operations:

imgctl auth-check \
  --server imgctl.example.com:9999 \
  --dex-url https://dex.example.com

Configuration

Environment Names

imgctl uses user-defined environment names for source and destination. The names are arbitrary — call them whatever makes sense for your organization. The server maps names to concrete registry URLs and S3 bucket suffixes via environment variables.

Container Registries (Server-Side)

Set IMGCTL_REGISTRY_<NAME> for each environment:

export IMGCTL_REGISTRY_DEV="123456789012.dkr.ecr.us-east-1.amazonaws.com"
export IMGCTL_REGISTRY_PROD="234567890123.dkr.ecr.us-east-1.amazonaws.com"
export IMGCTL_REGISTRY_INT="345678901234.dkr.ecr.us-east-1.amazonaws.com"
export IMGCTL_ECR_REGION="us-east-1"

The client uses --source dev --destination prod and the server resolves those names to the corresponding registry URLs.

SPA / CDN Buckets (Server-Side)

Set IMGCTL_SPA_<NAME> for each environment. The value is the bucket suffix — bucket names are constructed as <spa-name>-<suffix>:

export IMGCTL_SPA_DEV="dev-01"
export IMGCTL_SPA_PROD="prod-01"
export IMGCTL_SPA_STAGE="stage-01"

With these, imgctl promote spa:my-dashboard:v0.1.42 --source dev --destination prod copies s3://my-dashboard-dev-01/v0.1.42/ to s3://my-dashboard-prod-01/v0.1.42/.

Slack Notifications (Server-Side)

export IMGCTL_SLACK_WEBHOOK_URL="https://hooks.slack.com/services/YOUR/WEBHOOK/URL"

Omit to disable notifications.

Default Source Fallback (Server-Side)

If the client doesn't send a source environment, the server falls back to IMGCTL_DEFAULT_SOURCE:

export IMGCTL_DEFAULT_SOURCE="dev"

The server does not have a default destination — if the client doesn't specify one, the request fails.

Building with Compile-Time Defaults

Organizations can bake in defaults at build time via ldflags so users don't have to configure environment variables or pass flags for common operations.

Available Compile-Time Variables

Variable ldflags Path Purpose
defaultSource github.com/nikogura/imgctl/cmd.defaultSource Default --source on the client
defaultDestination github.com/nikogura/imgctl/cmd.defaultDestination Default --destination on the client
defaultClientID github.com/nikogura/imgctl/cmd.defaultClientID OAuth2 client ID for Dex
defaultClientSecret github.com/nikogura/imgctl/cmd.defaultClientSecret OAuth2 client secret for Dex

Example Build

go build -ldflags "\
  -X github.com/nikogura/imgctl/cmd.defaultSource=dev \
  -X github.com/nikogura/imgctl/cmd.defaultDestination=prod \
  -X github.com/nikogura/imgctl/cmd.defaultClientID=your-client-id \
  -X github.com/nikogura/imgctl/cmd.defaultClientSecret=your-client-secret" \
  -o imgctl .

With these defaults baked in, a user just types:

imgctl promote myapp:v1.0.0

and it promotes from dev to prod with the correct Dex credentials.

Precedence

For each configurable value, the precedence is (highest to lowest):

  1. CLI flag (--source, --destination, --client-id, etc.)
  2. Environment variable (IMGCTL_DEFAULT_SOURCE, IMGCTL_DEFAULT_DESTINATION, IMGCTL_CLIENT_ID, etc.)
  3. Compile-time default (baked in via ldflags)

CI/CD

The GitHub Actions workflow (.github/workflows/ci.yml) handles:

  • Test + Lint on every push to main and on PRs to main
  • Auto-versioning by incrementing the patch version from the last git tag
  • Cross-compilation of release binaries (darwin/amd64, darwin/arm64, linux/amd64)
  • Auto-tagging and GitHub Release creation on merge to main

To bake in defaults during CI, set these as GitHub repository secrets:

  • IMGCTL_DEFAULT_CLIENT_ID
  • IMGCTL_DEFAULT_CLIENT_SECRET

Source and destination defaults can be added to the build step in the workflow directly since they aren't secret.

Protocol Buffer Definition

The gRPC service is defined in pkg/imgctl/proto/imgctl.proto:

service ImgCtl {
  rpc Promote(PromoteRequest) returns (PromoteResponse) {}
  rpc AuthCheck(AuthCheckRequest) returns (AuthCheckResponse) {}
}

To regenerate after modifying the .proto file:

make proto

Development

Prerequisites

  • Go 1.24+
  • Protocol Buffer compiler (protoc)
  • protoc-gen-go and protoc-gen-go-grpc plugins
  • crane CLI tool (for image copying)
  • AWS CLI (for ECR authentication)
  • namedreturns linter (go install github.com/nikogura/namedreturns@latest)
  • golangci-lint

Running Tests

make test

Linting

make lint

Building

make build

Security Considerations

  • Authentication: Uses SSH-signed JWTs exchanged for OIDC tokens via Dex
  • Authorization: Group-based access control enforced at the server
  • Audit Logging: All operations are logged with user attribution
  • Token Validation: Server validates OIDC token signature, issuer, audience, and expiration
  • JWKS Caching: Public keys are cached for 5 minutes to reduce load on Dex

Production Recommendations:

  1. Enable TLS for gRPC connections (currently uses insecure credentials for development)
  2. Deploy behind a load balancer with proper TLS termination
  3. Configure firewall rules to restrict access to the gRPC port
  4. Rotate Dex signing keys regularly
  5. Monitor and alert on authentication failures
  6. Use separate OIDC audiences for different environments

License

Apache License 2.0. See LICENSE for details.

Authors

  • Nik Ogura

Acknowledgments

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages