Skip to content
FullStackS-GmbHPublic

About

A tool that moves {HELM Charts/Container Images/Git Repos} from A -> B

Resources

Contributing

Security policy

Stars

2 stars

Watchers

0 watching

Forks

Latest commit

 

History

60 Commits

Folders and files

Repository files navigation

Universal Airgapper

A Go CLI tool for synchronizing container images, Helm charts, and Git repositories across air-gapped environments.

Build Release License


What It Does

Universal Airgapper reads a YAML configuration file listing artifacts (container images, Helm charts, Git repos) with their source and destination, then copies each artifact from source to destination. It is designed for environments where registries sit behind an air gap and artifacts must be moved in a controlled, automated way.

Features

  • Sync container images between any Docker v2-compatible registries (Docker Hub, GHCR, Harbor, Nexus, private registries)
  • Sync Helm charts between OCI registries and legacy HTTP chart repositories
  • Sync Git repositories between any hosting services (GitHub, GitLab, Bitbucket, Azure Repos, self-hosted) via HTTPS or SSH
  • Helm image extraction -- render Helm charts and extract all container image references, generating a ready-to-use image sync config for air-gapped registry migration
  • Regex patterns for tags, chart versions, and git refs -- sync all matching versions automatically
  • Generic command scanner -- run any external security/compliance tool before promoting an artifact
  • Push modes: skip (default), force, overwrite -- control what happens when an artifact already exists at the destination
  • Dry-run mode -- preview what would be synced without making any changes
  • Multi-file configuration -- split config across multiple YAML files, merged at load time
  • Flexible credential management -- file-based credentials, organized by type, with SSH key support
  • Multiple deployment targets: run locally, in Docker, as a Kubernetes Job/CronJob, or in GitHub Actions / GitLab CI
  • Structured JSON logging via slog
  • Single statically linked binary, zero runtime dependencies

Quick Start

Install

The install script downloads the release archive for your platform, verifies its SHA-256 checksum against the published checksums file, and installs the binary. When cosign is on your PATH, it also verifies the archive's signature against cosign.pub before installing.

curl -fsSL https://raw.githubusercontent.com/fullstacks-gmbh/airgapper/main/install.sh | sh

The script asks for confirmation before it writes anything. It needs curl, tar, and jq, plus one of sha256sum, shasum, or openssl for the checksum verification.

Without AIRGAPPER_INSTALL_DIR, it installs into the first of ~/.local/bin, ~/bin, or /usr/local/bin that exists and is on your PATH.

Control the script with these environment variables.

Variable Default Effect
AIRGAPPER_VERSION latest Install a specific release, with or without the v prefix
AIRGAPPER_INSTALL_DIR auto Target directory, created if missing
AIRGAPPER_OS uname -s Override the platform (linux, darwin, windows)
AIRGAPPER_ARCH uname -m Override the architecture (amd64, arm64)
AIRGAPPER_YES 0 Skip the confirmation prompt
AIRGAPPER_QUIET 0 Suppress progress output
AIRGAPPER_SKIP_CHECKSUM 0 Install without checksum verification, not recommended
AIRGAPPER_SKIP_SIGNATURE 0 Skip signature verification even when cosign is installed
AIRGAPPER_COSIGN_KEY repo key Local path or URL of the public key to verify against
AIRGAPPER_COSIGN_OFFLINE 0 Verify without contacting the Rekor transparency log
NO_COLOR unset Disable colored output

Signature verification is best effort. The script skips it when cosign is missing or when the release carries no signature bundle, and it fails the install when cosign is present and verification does not pass.

# Pin a version and install unattended into a custom directory
curl -fsSL https://raw.githubusercontent.com/fullstacks-gmbh/airgapper/main/install.sh \
  | AIRGAPPER_VERSION=1.4.3 AIRGAPPER_YES=1 AIRGAPPER_INSTALL_DIR=~/.local/bin sh

Alternatively, download an archive from GitHub Releases or build from source:

# Build from source (requires Go 1.26.3+)
CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o airgapper ./cmd/airgapper

Or use the container image:

docker pull ghcr.io/fullstacks-gmbh/airgapper:latest

Verify signatures

Release archives, the checksums file, and the container image are signed with cosign using the key pair whose public half is cosign.pub in this repository. The install script performs this check automatically when cosign is available; the steps below cover manual verification of a downloaded artifact.

Each signed artifact has a matching .sigstore.json bundle in the release assets.

VERSION=1.4.3
BASE=https://github.com/fullstacks-gmbh/airgapper/releases/download/v${VERSION}
ARCHIVE=airgapper_${VERSION}_linux_amd64.tar.gz

# Public key, archive, and signature bundle
curl -fsSLO https://raw.githubusercontent.com/fullstacks-gmbh/airgapper/main/cosign.pub
curl -fsSLO "${BASE}/${ARCHIVE}"
curl -fsSLO "${BASE}/${ARCHIVE}.sigstore.json"

cosign verify-blob \
  --key cosign.pub \
  --bundle "${ARCHIVE}.sigstore.json" \
  "${ARCHIVE}"

A successful run prints Verified OK. Any other output means the archive does not match the signature and must not be used.

Verify the checksums file the same way, then check the archive hashes against it:

curl -fsSLO "${BASE}/airgapper_${VERSION}_checksums.txt"
curl -fsSLO "${BASE}/airgapper_${VERSION}_checksums.txt.sigstore.json"

cosign verify-blob \
  --key cosign.pub \
  --bundle "airgapper_${VERSION}_checksums.txt.sigstore.json" \
  "airgapper_${VERSION}_checksums.txt"

sha256sum --check --ignore-missing "airgapper_${VERSION}_checksums.txt"

Verify the container image by tag or by digest:

cosign verify --key cosign.pub ghcr.io/fullstacks-gmbh/airgapper:1.4.3

Signatures are also recorded in the public Rekor transparency log, which cosign contacts during verification. On hosts without internet access, add --insecure-ignore-tlog=true to verify against the public key alone.

Configure

Create a configuration file (e.g., config.config.config.airgapper.yaml):

resources:
  - type: image
    source: ubuntu
    destination: registry.internal.corp/mirror/ubuntu
    tags:
      - "22.04"
      - "24.04"

Run

# Local binary
airgapper sync --config ./config.config.airgapper.yaml

# Docker
docker run --rm \
  -v $(pwd)/config.config.airgapper.yaml:/mnt/config.config.airgapper.yaml \
  ghcr.io/fullstacks-gmbh/airgapper:latest \
  sync --config /mnt/

Configuration Reference

Configuration files must be named *.config.airgapper.yaml or *.config.airgapper.yml. When a folder is provided via --config, all matching files are discovered and merged (resource lists are concatenated; scanner definitions are merged by name).

See docs/configuration.md for the full reference.

Image Resources

Sync container images between Docker v2-compatible registries.

resources:
  - type: image                # also accepts "docker" for backward compat
    source: registry.example.com/team/app
    destination: internal-registry.corp/mirror/app
    push_mode: skip            # skip | force (default: skip)
    scanner_ref: trivy         # optional: run scanner before push
    source_credentials_ref: source-reg   # optional
    target_credentials_ref: internal-reg # optional
    tags:
      - "v1.0.0"
      - "v2\\..*"             # regex: all v2.x tags
      - "latest"

Helm Resources

Sync Helm charts between OCI registries and legacy HTTP chart repositories.

resources:
  - type: helm
    source_registry: registry-1.docker.io
    source_chart: bitnamicharts/postgresql
    destination_registry: internal-registry.corp
    destination_repo: charts
    # destination_chart: postgresql # optional target name override
    push_mode: skip            # skip | overwrite (default: skip)
    versions:
      - "16.0.0"
      - "16\\..*"             # regex: all 16.x versions

The destination OCI artifact name is automatically derived from Chart.yaml, which also handles vendor artifacts whose repository name differs from the real chart name.

Chart pushes include application/vnd.oci.image.manifest.v1+json in the manifest body and deploy the final version tag directly. This supports Artifactory registries that otherwise mislabel untagged Helm manifests as Docker schema 1, causing Helm 4.0.4's digest-fetch/tag step to fail with mismatch response Content-Type. Chart configuration and layer media types stay compatible with Helm OCI clients. Set destination_chart only for an explicit name override.

Git Resources

Sync Git repositories between hosting services via HTTPS or SSH.

resources:
  - type: git
    source_repo: git@github.com:org/project.git
    destination_repo: git@internal-git.corp:mirror/project.git
    push_mode: skip            # skip | push | force (default: skip)
    source_credentials_ref: github-ssh        # optional
    target_credentials_ref: internal-git-ssh  # optional
    refs:
      - "main"
      - "release/.*"          # regex: all release branches
      - "v[0-9]+\\..*"        # regex: all version tags

Scanner Configuration

Define external scanner commands that run before an artifact is pushed. The scanner is generic -- any command-line tool that returns an exit code can be used.

scanners:
  - name: trivy
    command: "trivy image --exit-code 1 --severity HIGH,CRITICAL {registry}/{repository}:{tag}"
    success_code: 0            # expected exit code for "pass" (default: 0)
    timeout: 300               # max seconds (default: 300)

resources:
  - type: image
    source: ubuntu
    destination: internal-registry.corp/mirror/ubuntu
    scanner_ref: trivy         # references the scanner above
    tags:
      - "22.04"

Available placeholders in the scanner command:

Placeholder Description
{registry} Registry hostname (e.g., registry-1.docker.io)
{repository} Repository path (e.g., library/ubuntu)
{tag} Tag / version / ref being synced
{source} Full source reference (e.g., docker.io/library/ubuntu:22.04)
{type} Resource type (image, helm, git)

Full Example

scanners:
  - name: trivy
    command: "trivy image --exit-code 1 --severity HIGH,CRITICAL {registry}/{repository}:{tag}"
    success_code: 0
    timeout: 300

resources:
  # Container images
  - type: image
    source: ubuntu
    destination: registry.internal.corp/mirror/ubuntu
    scanner_ref: trivy
    tags:
      - "22.04"
      - "24.04"

  - type: image
    source: registry.example.com/team/app
    destination: registry.internal.corp/mirror/app
    push_mode: force
    tags:
      - "v1\\..*"

  # Helm charts
  - type: helm
    source_registry: registry-1.docker.io
    source_chart: bitnamicharts/mariadb
    destination_registry: registry.internal.corp
    destination_repo: charts
    versions:
      - "20.2.1"
      - "20.2.0"

  # Git repositories
  - type: git
    source_repo: git@github.com:org/project.git
    destination_repo: git@internal-git.corp:mirror/project.git
    push_mode: skip
    refs:
      - "main"
      - "v.*"

Credentials Reference

Credentials are provided in separate YAML files, organized by type. Provide them via --credentials (single file or folder). When pointing to a folder, files must match *.creds.airgapper.yaml or *.creds.airgapper.yml.

See docs/credentials.md for the full reference.

image:
  - name: docker-hub
    username: myuser
    password: mytoken
  - name: internal-registry
    username: serviceaccount
    password: s3cret

helm:
  - name: chart-registry
    username: helmuser
    password: helmpass

git:
  - name: github-ssh
    ssh_key_path: /path/to/id_ed25519
  - name: github-https
    username: git
    password: ghp_xxxxxxxxxxxx
  - name: azure-repos
    username: AzureReposAuthnSucks    # Azure Repos workaround
    password: REPO_AUTH_HEADER        # env var name containing the auth header

Credential resolution order:

  1. By explicit reference (source_credentials_ref / target_credentials_ref in resource config)
  2. By hostname match (credential name matches the registry/host)
  3. Anonymous access (no credentials found)

CLI Reference

Commands

airgapper sync            Synchronize artifacts from source to destination
airgapper validate        Check configuration and credentials without syncing
airgapper helm images     Extract container image references from Helm charts
airgapper version         Print version, commit, and build date

Global Flags

Flag Short Env Var Default Description
--config -c AIRGAPPER_CONFIG (none) Path to config file or folder
--credentials AIRGAPPER_CREDENTIALS (none) Path to credentials file or folder
--debug -d AIRGAPPER_DEBUG false Enable debug logging (JSON, DEBUG level)
--dry-run AIRGAPPER_DRY_RUN false Disable all write/push operations
--log-format AIRGAPPER_LOG_FORMAT json Log format: json or text
--dry-run-log AIRGAPPER_DRY_RUN_LOG (auto) Path for the dry-run log file
--timeout AIRGAPPER_TIMEOUT 0 Overall run timeout in seconds (0 = off)
--retry-attempts AIRGAPPER_RETRY_ATTEMPTS 3 Tries per network operation, including the first (1 = no retry)
--retry-delay AIRGAPPER_RETRY_DELAY 2 Seconds before the first retry, doubled for each further retry (with jitter, max 30 s)

Retries

Network operations that fail with a transient error are retried with exponential backoff and jitter: HTTP 408, 429 and 5xx responses, timeouts, reset or refused connections, and truncated responses. Credential errors (401/403), missing artifacts (404) and a cancelled run are never retried, and --timeout still bounds the whole run, including the waits between tries. A retry that is logged shows the operation, the attempt and the delay.

validate and --output json

airgapper validate checks the configuration and credentials without contacting any registry or host. It reports every problem in one run:

  • unknown or misspelled keys, missing required fields, invalid push modes and unknown scanner_ref names
  • options that do not apply to the resource type (for example policy_path on a Helm or Git resource)
  • credential references (source_credentials_ref, target_credentials_ref) that do not exist for the resource type, or are used without --credentials
  • files that do not exist: policy_path, the ca.crt in *_ca_cert directories, and the ssh_key_path of referenced credentials

It exits 0 when everything is fine and 2 otherwise.

sync and validate accept --output json (AIRGAPPER_OUTPUT, default text). With it, stdout carries exactly one JSON document and logs stay on stderr, so a pipeline can read the result without parsing text:

airgapper validate --config ./configs/ --credentials ./creds/ --output json
airgapper sync --config ./configs/ --dry-run --output json | jq '.summary'

helm images has its own --output flag (the path of the file to write) and is not affected.

helm images Flags

Flag Env Var Required Description
--output, -o AIRGAPPER_HELM_IMAGES_OUTPUT yes Path to write the generated image config YAML
--target-credentials-ref AIRGAPPER_HELM_IMAGES_TARGET_CREDENTIALS_REF yes Name of a helm credential entry whose name field is the destination registry hostname

The command reads all helm resources from the config, pulls and renders each chart version with its default values, extracts every image: reference from the rendered manifests, and writes a ready-to-use airgapper image config YAML to --output. The output file can be fed directly to airgapper sync to mirror those images into an air-gapped registry.

airgapper helm images \
  --config ./configs/ \
  --credentials ./creds/ \
  --output ./image-sync.config.airgapper.yaml \
  --target-credentials-ref my-internal-registry

Exit Codes

Code Meaning
0 All resources synced successfully
1 Sync completed with one or more failures
2 Usage or configuration error (also validate finding problems)

Examples

# Sync with a single config file
airgapper sync --config config.config.airgapper.yaml

# Sync with a folder of config files and credentials
airgapper sync --config ./configs/ --credentials ./creds/

# Dry run with debug logging
airgapper sync --config ./configs/ --credentials ./creds/ --dry-run --debug

# Using environment variables
export AIRGAPPER_CONFIG=./configs/
export AIRGAPPER_CREDENTIALS=./creds/
export AIRGAPPER_DRY_RUN=true
airgapper sync

# Print version
airgapper version

Deployment Options

Local

airgapper sync --config ./configs/ --credentials ./creds/

Docker

docker run --rm \
  -v $(pwd)/configs:/mnt/configs \
  -v $(pwd)/creds:/mnt/creds \
  ghcr.io/fullstacks-gmbh/airgapper:latest \
  sync --config /mnt/configs/ --credentials /mnt/creds/

For Git over SSH, mount your SSH configuration:

docker run --rm \
  -v $(pwd)/.ssh:/home/nonroot/.ssh:ro \
  -v $(pwd)/configs:/mnt/configs \
  -v $(pwd)/creds:/mnt/creds \
  ghcr.io/fullstacks-gmbh/airgapper:latest \
  sync --config /mnt/configs/ --credentials /mnt/creds/

Kubernetes Job

Mount config via ConfigMap and credentials via Secrets. Example manifests are in k8s/.

apiVersion: batch/v1
kind: Job
metadata:
  name: airgapper-job
spec:
  template:
    spec:
      containers:
        - name: airgapper
          image: ghcr.io/fullstacks-gmbh/airgapper:latest
          command: [ "/airgapper" ]
          args: [ "sync", "--config", "/mnt/config/", "--credentials", "/mnt/creds/" ]
          volumeMounts:
            - name: config-volume
              mountPath: /mnt/config/config.yaml
              subPath: config.yaml
            - name: secrets-image
              mountPath: /mnt/creds/image.yaml
              subPath: image.yaml
      restartPolicy: Never
      volumes:
        - name: config-volume
          configMap:
            name: airgapper-config
        - name: secrets-image
          secret:
            secretName: airgapper-secrets-image

Kubernetes CronJob

Same as Job, with a schedule:

apiVersion: batch/v1
kind: CronJob
metadata:
  name: airgapper-cronjob
spec:
  schedule: "0 0,12 * * *"   # Twice daily
  jobTemplate:
    spec:
      template:
        spec:
          containers:
            - name: airgapper
              image: ghcr.io/fullstacks-gmbh/airgapper:latest
              command: [ "/airgapper" ]
              args: [ "sync", "--config", "/mnt/config/", "--credentials", "/mnt/creds/" ]
              # ... volume mounts as above

GitHub Actions

Use the reusable workflow from this repository. See docs/github-actions.md.

jobs:
  sync:
    uses: fullstacks-gmbh/universal-airgapper/.github/workflows/universal-airgapper.yml@main
    with:
      config-folder: ${{ github.workspace }}/configs
    secrets:
      GHCR_PAT: ${{ secrets.GHCR_PAT }}

GitLab CI

Use the CI component template. See docs/gitlab-ci.md.

include:
  - component: <airgapper-repo-group>/airgapper@1
    inputs:
      stage: run
      config-folder: ${CI_PROJECT_DIR}
      credentials-file: ${UNIVERSAL_AIRGAPPER_CREDS}

Architecture Overview

Universal Airgapper follows Hexagonal Architecture (Ports & Adapters) with the Strategy pattern for pluggable transport backends.

cmd/airgapper/main.go        # Thin entrypoint
internal/
  cli/                        # Cobra commands (root, sync, helm images, version)
  config/                     # YAML config loading, validation, merging
  credentials/                # File-based credential store
  domain/                     # Core types, interfaces, errors (zero deps)
  helmimages/                 # Helm chart rendering and image reference extraction
  transport/
    image/                    # Container image sync (containers/image v5)
    helm/                     # Helm chart sync (Helm v4 SDK)
    git/                      # Git repo sync (go-git v5)
  scanner/                    # Generic external command scanner
  sync/                       # Orchestration engine
  logging/                    # Structured logging (slog)

For the full C4 architecture model and detailed design, see docs/architecture/.

Development

Prerequisites

  • Go 1.26.3+
  • direnv (optional, for automatic env setup)
  • Nix (optional, provides reproducible toolchain via flake.nix)
  • Docker (for container builds)

Build

# Development build
go build ./cmd/airgapper

# Production build (static, stripped)
CGO_ENABLED=0 go build \
  -trimpath \
  -ldflags="-s -w -X main.version=$(git describe --tags) -X main.commit=$(git rev-parse HEAD)" \
  -o airgapper \
  ./cmd/airgapper

Test

go test ./...                # Unit tests
go test -race ./...          # With race detector
go test -tags=integration ./... # Integration tests

Lint

golangci-lint run

Contributing

Contributions are welcome. See CONTRIBUTING.md for development setup, coding standards, testing guidelines, and commit conventions.

License

This project is licensed under the GNU General Public License v3.0.

About

A tool that moves {HELM Charts/Container Images/Git Repos} from A -> B

Resources

Contributing

Security policy

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages