Generated: 2026-03-06 | Version: 2.0 | Author: Romain G.
This document outlines the overall project architecture for Buncker, a surgical Docker layer synchronization system for air-gapped environments. It serves as the guiding architectural blueprint for AI-driven development, ensuring consistency and adherence to chosen patterns and technologies.
N/A - Greenfield project, no starter template. Built from scratch in pure Python with .deb packaging for both offline and online components.
| Date | Version | Description | Author |
|---|---|---|---|
| 2026-03-04 | 1.0 | Initial architecture document | Romain G. |
| 2026-03-06 | 2.0 | Added Admin API authentication, TLS, LAN client operations, streaming import | Romain G. |
Buncker is a surgical Docker layer synchronization system for air-gapped environments, architected as two independent components communicating exclusively via encrypted files on physical media (USB). The offline side is a permanent HTTP daemon exposing the OCI Distribution API (for Docker pulls) and an administration API (Dockerfile analysis, manifest generation, import, GC, logs). The online side is a stateless one-shot CLI that fetches missing blobs from public registries. The entire system runs on Python 3 + stdlib with python3-cryptography as the sole external dependency, packaged as .deb. Transfer channel security is ensured by AES-256 + HMAC-SHA256 symmetric crypto derived from a BIP-39 mnemonic shared once.
- Architectural Style: Permanent HTTP daemon (offline) + one-shot CLI (online). Two fully decoupled components.
- Repository: Monorepo with two packages (
buncker/andbuncker_fetch/) + shared code (shared/) - Service Architecture: The offline registry is a permanent daemon exposing:
- OCI Distribution API (pull subset) for Docker build clients
- Admin API:
analyze,generate-manifest,import,status,gc,logs
- Primary Flow:
- Operator analyzes Dockerfile(s) → list of missing blobs
- Generation of
request.json.enc(encrypted) → USB - Online:
buncker-fetch fetch request.json.enc→ downloads →response.tar.enc→ USB - Offline: import → verification → store → Docker builds work
- Key Decisions: No internet fallback (miss = error), manual GC only, deduplication at manifest level, symmetric crypto (no PKI)
- V2 - LAN Client Operations: Optional Bearer token authentication on admin API via
buncker api-setup. Two access levels (read-only, admin). TLS mandatory when auth enabled. LAN clients interact via curl - no new binary needed
graph TB
subgraph "OFFLINE - Isolated LAN"
BC1[Build Client 1] --> REG
BC2[Build Client 2] --> REG
BCN[Build Client N] --> REG
ADMIN[Operator] -->|CLI / Admin API| REG
REG[buncker daemon<br/>Permanent HTTP]
REG --> STORE[(Store<br/>OCI blobs)]
REG --> RESOLVER[Resolver<br/>Dockerfile]
REG --> LOGS[(Logs JSONL)]
end
subgraph "TRANSFER - USB"
REQ[request.json.enc<br/>AES-256 + HMAC]
RESP[response.tar.enc<br/>AES-256 + HMAC]
end
subgraph "ONLINE - Connected machine"
FETCH[buncker-fetch<br/>CLI one-shot]
CACHE[(Local cache<br/>blobs)]
FETCH --> CACHE
end
REG -->|generates| REQ
REQ -->|USB| FETCH
FETCH -->|fetch by digest| DH[Docker Hub]
FETCH -->|fetch by digest| GHCR[ghcr.io]
FETCH -->|fetch by digest| OTHER[Other registries]
FETCH -->|generates| RESP
RESP -->|USB| REG
REG -->|import + verify| STORE
-
Daemon HTTP Pattern: buncker runs as a permanent systemd service, serving the OCI API to Docker clients and exposing administration endpoints. Rationale: Build clients need to pull at any time without manual intervention.
-
CLI One-Shot Pattern: buncker-fetch is purely stateless, one-shot execution by the operator. Rationale: No daemon to maintain on the connected machine.
-
OCI Distribution Spec (subset): The offline registry implements only the endpoints necessary for
docker pull(GET/HEAD manifest, GET/HEAD blob). Rationale: Minimal subset = less code, fewer bugs. -
Store Pattern (OCI Image Layout): Blobs are stored per OCI Image Layout standard (
blobs/sha256/{digest}). Rationale: Standard format, verifiable by digest, compatible with other OCI tools. -
Shared-Nothing Transfer: The two sides never communicate over a network. The only channel is an encrypted file on USB. Rationale: Real air-gap, no hidden channel risk, full auditability.
-
Symmetric Crypto (Pre-Shared Key): AES-256 + HMAC-SHA256 derived from a BIP-39 mnemonic shared once. Rationale: No PKI to manage, no certificates to renew, adapted to a context where both endpoints are controlled by the same organization.
-
Delta Sync: Only missing blobs are requested. The transfer manifest is a diff between the local store and Dockerfile needs. Rationale: Core value proposition vs Hauler (bulk snapshot). Bandwidth and time savings.
-
Optional Bearer Token Auth (V2): Two-tier Bearer tokens (read-only / admin) on
/admin/*endpoints, activated viabuncker api-setup./v2/*OCI endpoints remain unauthenticated. Rationale: LAN clients (VMs, remote racks) need remote access to admin operations. Tokens are cryptographically random (256-bit), not derived from the mnemonic - separate security domain. -
TLS Enforcement with Auth: When auth is enabled, TLS is mandatory (operator-provided cert or auto-signed). Rationale: Bearer tokens in cleartext over HTTP = interceptable. Auto-signed reuses existing
export-camechanism. -
Streaming Upload Pattern (V2): Large file imports (
response.tar.enc, potentially multi-GB) use PUT with chunked write-to-disk andContent-Rangeresume support. Rationale: Buncker may run in a VM/rack/DC where USB is impractical. LAN transfers of multi-GB files must not load entirely in memory and must survive interruptions.
N/A - Buncker is 100% on-premise by design. No cloud provider, no SaaS.
| Category | Technology | Version | Purpose | Rationale |
|---|---|---|---|---|
| Language | Python | >=3.11 | Sole language for both packages | Rich stdlib, native on Debian 12 |
| Crypto | python3-cryptography | >=41.0 (apt) | AES-256-GCM, optional TLS cert gen | OS-packaged dependency. Installed via apt, not pip |
| Config | python3-yaml (PyYAML) | >=5.0 (apt) | Docker Compose YAML parsing | OS-packaged dependency. Installed via apt, not pip |
| HTTP Server | ThreadingHTTPServer + ThreadPoolExecutor | stdlib | Permanent daemon for OCI + Admin API | Zero dependency. Bounded thread pool (max_workers=16 configurable) |
| HTTP Client | urllib.request | stdlib | Fetch blobs from public registries | Native HTTPS, certificate verification by default |
| Hashing | hashlib | stdlib | SHA256 (blob digests), PBKDF2 (key derivation) | Standard, performant |
| Auth Signatures | hmac | stdlib | HMAC-SHA256 (transfer manifest integrity) | stdlib, standard crypto |
| Concurrency | concurrent.futures | stdlib | ThreadPoolExecutor for parallel fetch | stdlib, simple, sufficient for I/O-bound |
| Logging | logging | stdlib | Structured JSON Lines logs | stdlib, configurable handlers |
| Arg Parsing | argparse | stdlib | CLI for both tools | stdlib, subcommands support |
| Packaging (offline) | dpkg / .deb | - | buncker distribution on Debian/Ubuntu | Native apt dependency management |
| Packaging (online) | dpkg / .deb | - | buncker-fetch distribution on Debian/Ubuntu | Same format, consistency |
| Service Manager | systemd | - | buncker offline daemon | Standard on Debian 12+, auto-restart, journald |
| Testing | pytest | 8.x | Unit and integration tests | Dev only, not in .deb |
| Linting | ruff | 0.8.x | Linting + formatting | Fast, replaces flake8+black+isort |
Purpose: Represents a layer or config blob in the local store
Key Attributes:
digest: string (sha256:abc123...) - unique identifier, filenamesize: int - size in bytesmedia_type: string - OCI typeimage_refs: list[string] - images referencing this blobfirst_imported: datetime ISOlast_requested: datetime ISO - last pull by a build clientrequest_count: int - number of pullsgc_status: enum (active|gc_candidate)
Persistence: Blob = file in store/blobs/sha256/{digest_hex}. Metadata = JSON sidecar store/meta/{digest_hex}.json
Purpose: Image manifest for a given platform (link between tag and blobs)
Key Attributes:
digest,registry,repository,tag,platformconfig_digest: string - config blob digestlayers: list[string] - ordered layer digestscached_at: datetime ISO
Persistence: store/manifests/{registry}/{repository}/{tag}/{platform}.json
Purpose: Transfer manifest generated by offline, consumed by online
Key Attributes:
version: string (1)buncker_version: string - for auto-update checkgenerated_at: datetime ISOsource_id: stringblobs: list[{registry, repository, digest, size, media_type}]
Persistence: .json.enc file (AES-256 encrypted JSON + HMAC)
Purpose: OCI package containing requested blobs, produced by online
Key Attributes:
oci-layout,index.json,blobs/sha256/...,MANIFEST.sig,ERRORS.json
Persistence: .tar.enc archive (AES-256 encrypted tar + HMAC)
Purpose: Persistent crypto configuration on both sides
Offline: source_id, mnemonic_hash, salt, created_at, rotated_at, grace_period_days
Online: derived_key_check, salt, registries (credentials via env vars)
Persistence: config.json (offline: /etc/buncker/, online: ~/.buncker/)
Purpose: Result of Dockerfile analysis - not persisted
Key Attributes:
source_path,build_args,images: list[ResolvedImage] withraw,resolved,registry,repository,tag,digest,platform,alias,is_internal,is_private,missing_blobs
Purpose: Bearer tokens for admin API authentication
Key Attributes:
readonly: string - 256-bit hex token for read-only accessadmin: string - 256-bit hex token for full admin access
Persistence: /etc/buncker/api-tokens.json (mode 0600). Only exists after buncker api-setup.
Responsibility: Central point of the isolated LAN. Serves Docker images to build clients and exposes store administration.
Key Interfaces:
- OCI Distribution API (pull subset) - configurable port (default 5000), always unauthenticated
GET /v2/- version checkGET /v2/{name}/manifests/{reference}- fetch manifestHEAD /v2/{name}/manifests/{reference}- check existenceGET /v2/{name}/blobs/{digest}- fetch blobHEAD /v2/{name}/blobs/{digest}- check blob existence
- Admin API - same port,
/admin/prefix, optional Bearer token auth (V2)POST /admin/analyze,POST /admin/generate-manifest,POST /admin/import(local POST),PUT /admin/import(remote streaming)GET /admin/status,GET /admin/gc/report,POST /admin/gc/execute,GET /admin/logs
- Auth Middleware (V2) - validates Bearer tokens on
/admin/*whenapi.enabled: true- Read-only token:
status,logs,gc/report - Admin token: all
/admin/*endpoints
- Read-only token:
- Management CLI - local-only commands (not exposed via HTTP)
buncker api-setup- generate tokens, activate TLSbuncker api-show readonly|admin- display tokenbuncker api-reset readonly|admin- regenerate token
Dependencies: None. Self-contained.
Responsibility: Downloads missing blobs from public registries, produces response.tar.enc.
Key Interfaces:
buncker-fetch pair- mnemonic setupbuncker-fetch inspect <request.json.enc>- display contentsbuncker-fetch fetch <request.json.enc> [--output] [--parallelism N]buncker-fetch status- local cache statebuncker-fetch cache clean [--older-than Nd]
Dependencies: Outbound HTTPS to public registries.
Responsibility: All crypto logic, identical on both sides.
Interfaces: generate_mnemonic(), derive_keys(), encrypt(), decrypt(), sign(), verify()
Dependencies: python3-cryptography (apt) for AES-256-GCM. Rest = stdlib.
Responsibility: OCI Image Layout manipulation.
Interfaces: parse_manifest(), parse_index(), build_image_layout(), verify_blob(), select_platform()
Dependencies: stdlib only.
Responsibility: Static Dockerfile analysis, FROM resolution.
Interfaces: parse_dockerfile(), parse_args(), resolve_from()
Responsibility: Reads cached manifests. NO network requests.
Responsibility: Auth discovery + Bearer token + fetch from public registries.
Responsibility: OCI blob store management (blobs + metadata + GC).
Interfaces: has_blob(), get_blob(), import_blob(), list_missing(), update_metadata(), gc_report(), gc_execute()
graph TB
subgraph "buncker (offline daemon)"
SERVER[HTTP Server<br/>OCI API + Admin API]
RESOLVER[resolver<br/>Parse Dockerfile]
REGCLIENT_OFF[registry_client<br/>Cache manifests]
STORE[store<br/>Blobs + Meta + GC]
SERVER --> RESOLVER
SERVER --> STORE
RESOLVER --> REGCLIENT_OFF
RESOLVER --> STORE
REGCLIENT_OFF --> STORE
end
subgraph "shared"
CRYPTO[crypto<br/>AES + HMAC + PBKDF2]
OCI[oci<br/>OCI Image Layout]
end
subgraph "buncker-fetch (online CLI)"
CLI[CLI entry point]
FETCHER[fetcher<br/>Download blobs]
REGCLIENT_ON[registry_client<br/>Auth + fetch]
CACHE[cache<br/>Local blob cache]
CLI --> FETCHER
FETCHER --> REGCLIENT_ON
FETCHER --> CACHE
end
SERVER --> CRYPTO
SERVER --> OCI
CLI --> CRYPTO
CLI --> OCI
FETCHER --> OCI
REGCLIENT_ON -->|HTTPS| REGISTRIES[Docker Hub<br/>ghcr.io<br/>quay.io]
DOCKER[Build Clients] -->|HTTP/HTTPS| SERVER
LANCLIENT[LAN Clients<br/>curl] -->|HTTPS + Bearer| SERVER
All public registries follow the OCI Distribution Spec. buncker-fetch implements a single client with auth discovery:
1. GET /v2/ → 401 + Www-Authenticate header → parse realm/service/scope
2. GET {realm}?service={service}&scope={scope} → Bearer token
3. GET /v2/{name}/manifests/{ref} + OCI Accept headers
4. GET /v2/{name}/blobs/{digest} → binary stream
Registries supported: docker.io, ghcr.io, quay.io, gcr.io, any OCI-compliant custom registry.
Retry policy: 3 attempts, exponential backoff (1s, 3s, 9s). Timeout: 30s connect, 120s read. Rate limits: Docker Hub 100 pulls/6h anonymous, 200/6h authenticated. Mitigated by local manifest cache.
sequenceDiagram
participant OP as Operator
participant D as buncker daemon
participant RES as resolver
participant RC as registry_client (cache)
participant ST as store
participant CR as crypto
OP->>D: POST /admin/analyze {dockerfile, build_args}
D->>RES: parse_dockerfile(path, build_args)
RES->>RES: Parse pre-FROM ARGs + substitution
RES->>RES: Extract FROM lines, detect internal aliases
loop For each external image
RES->>RC: get_manifest(registry, repo, tag)
RC->>ST: Read cached manifest
alt Cached
ST-->>RES: OCIManifest
else Not cached
RC-->>RES: None (error: unknown manifest)
end
RES->>ST: list_missing(layer_digests)
ST-->>RES: missing blobs list
end
RES-->>D: list[ResolvedImage] with missing_blobs
D-->>OP: Analysis report
OP->>D: POST /admin/generate-manifest
D->>CR: encrypt(request_json, aes_key)
CR-->>D: request.json.enc
D-->>OP: File ready → USB
sequenceDiagram
participant OP as Operator
participant CLI as buncker-fetch
participant CR as crypto
participant CA as cache
participant RC as registry_client
participant REG as Public registries
OP->>CLI: buncker-fetch fetch request.json.enc
CLI->>CR: decrypt + verify HMAC
alt Invalid HMAC
CR-->>CLI: ERROR
CLI-->>OP: Refused, nothing downloaded
end
CR-->>CLI: request JSON
loop For each blob (ThreadPoolExecutor)
CLI->>CA: has_blob(digest)?
alt Cached
CA-->>CLI: skip
else Not cached
CLI->>RC: authenticate(registry)
RC->>REG: Token exchange
CLI->>RC: fetch_blob(digest)
RC->>REG: GET blob
REG-->>RC: blob data
CLI->>CLI: verify SHA256
CLI->>CA: store_blob(digest, data)
end
end
CLI->>CR: sign + encrypt → response.tar.enc
CLI-->>OP: File ready → USB
sequenceDiagram
participant OP as Operator
participant D as buncker daemon
participant CR as crypto
participant ST as store
OP->>D: POST /admin/import {response.tar.enc}
D->>CR: decrypt + verify HMAC
alt Failed
D-->>OP: Refused
end
loop For each blob
D->>D: verify SHA256
alt Mismatch
D-->>OP: ERROR: corrupt blob
else OK
D->>ST: import_blob (atomic write)
end
end
D-->>OP: Import complete: N blobs, X MB
sequenceDiagram
participant DC as Docker client
participant D as buncker daemon
participant ST as store
DC->>D: GET /v2/
D-->>DC: 200 OK
DC->>D: GET /v2/{name}/manifests/{ref}
D->>ST: get_manifest
D-->>DC: 200 manifest (or 404)
loop For each layer
DC->>D: GET /v2/{name}/blobs/{digest}
D->>ST: get_blob + update_metadata
D-->>DC: 200 blob stream
end
sequenceDiagram
participant OFF as Operator (offline)
participant D as buncker daemon
participant ON as Operator (online)
participant F as buncker-fetch
OFF->>D: buncker setup
D->>D: generate_mnemonic() → 16 BIP-39 words (12 secret + 4 salt)
D->>D: derive_keys + save config
D-->>OFF: Display 16 words (write on paper)
Note over OFF,ON: Human channel (verbal, paper)
ON->>F: buncker-fetch pair
F-->>ON: Enter 16 words
ON->>F: word1 word2 ... word16
F->>F: derive_keys + save config
F-->>ON: Pairing OK
sequenceDiagram
participant OP as Operator (local)
participant D as buncker daemon
OP->>D: buncker api-setup [--cert cert.pem --key key.pem]
D->>D: Generate read-only token (256-bit)
D->>D: Generate admin token (256-bit)
alt Certificate provided
D->>D: Configure TLS with provided cert
else No certificate
D->>D: Generate auto-signed cert + CA
D-->>OP: WARNING: auto-signed certificate
end
D->>D: Save tokens to /etc/buncker/api-tokens.json (0600)
D->>D: Update config: api.enabled=true, tls=true
D-->>OP: Display read-only token
D-->>OP: Display admin token
Note over OP: Distribute tokens to LAN clients
sequenceDiagram
participant LC as LAN Client (curl)
participant D as buncker daemon
participant AUTH as Auth Middleware
participant RES as resolver
participant ST as store
LC->>D: POST /admin/analyze {dockerfile_content, build_args}
D->>AUTH: Validate Bearer token
alt Invalid/missing token
AUTH-->>LC: 401 Unauthorized
else Read-only token
AUTH-->>LC: 403 Forbidden
else Admin token
AUTH-->>D: OK
end
D->>RES: parse_dockerfile(content, build_args)
RES-->>D: AnalysisResult
D-->>LC: JSON analysis report
LC->>D: POST /admin/generate-manifest
D->>AUTH: Validate Bearer token (admin)
D-->>LC: request.json.enc (application/octet-stream)
sequenceDiagram
participant LC as LAN Client (curl)
participant D as buncker daemon
participant AUTH as Auth Middleware
participant CR as crypto
participant ST as store
LC->>D: PUT /admin/import (streaming body)
Note over LC,D: Headers: Authorization, X-Buncker-Checksum, Content-Range (optional)
D->>AUTH: Validate Bearer token (admin)
alt Invalid token
AUTH-->>LC: 401 Unauthorized
end
D->>D: Stream body to temp file (chunked)
alt Content-Range present (resume)
D->>D: Append to existing partial upload
end
D->>D: Verify X-Buncker-Checksum vs received file
alt Checksum mismatch
D-->>LC: 400 Upload integrity check failed
end
D->>CR: decrypt + verify HMAC
loop For each blob
D->>D: verify SHA256
D->>ST: import_blob (atomic write)
end
D-->>LC: JSON import summary
| Method | Path | Purpose | Auth |
|---|---|---|---|
| GET | /v2/ |
Version check | None |
| GET | /v2/{name}/manifests/{reference} |
Fetch manifest | None |
| HEAD | /v2/{name}/manifests/{reference} |
Check manifest existence | None |
| GET | /v2/{name}/blobs/{digest} |
Fetch blob | None |
| HEAD | /v2/{name}/blobs/{digest} |
Check blob existence | None |
Required headers on responses: Docker-Content-Digest, Content-Type, Content-Length.
OCI endpoints are always unauthenticated regardless of auth configuration.
| Method | Path | Purpose | Auth (V2) |
|---|---|---|---|
| POST | /admin/analyze |
Analyze Dockerfile(s) - accepts dockerfile_path (localhost) or dockerfile_content (remote) |
Admin |
| POST | /admin/generate-manifest |
Generate request.json.enc - returns file in response body | Admin |
| POST | /admin/import |
Import response.tar.enc (local CLI, multipart/form-data) | Admin |
| PUT | /admin/import |
Streaming upload of response.tar.enc (remote, curl -T) |
Admin |
| GET | /admin/status |
Store state + disk usage | Read-only |
| GET | /admin/health |
Health check (store integrity, disk, TLS cert expiry, uptime) | Read-only |
| GET | /admin/gc/report |
GC candidates report | Read-only |
| POST | /admin/gc/impact |
Impact analysis before deletion (affected images) | Admin |
| POST | /admin/gc/execute |
Execute GC (requires operator + digests) | Admin |
| GET | /admin/logs |
Query logs (filter by event, since, limit) | Read-only |
When api.enabled: true in config, all /admin/* requests require Authorization: Bearer <token>.
| Response | Condition |
|---|---|
| 401 Unauthorized | Missing or invalid token |
| 403 Forbidden | Valid read-only token on an admin-only endpoint |
When api.enabled: false (default, no api-setup run), all endpoints behave as V1 (no auth).
| Header | Required | Purpose |
|---|---|---|
Authorization: Bearer <token> |
Yes (when auth enabled) | Admin token |
X-Buncker-Checksum: sha256:<hex> |
Yes | Pre-decryption integrity check |
Content-Range: bytes <start>-<end>/<total> |
No | Resume partial upload |
Content-Length |
Yes | Total body size |
The daemon writes the body to disk in chunks (never loads entirely in memory). After upload, it verifies the checksum before running the standard import pipeline.
All API requests are logged with additional fields:
| Field | Values |
|---|---|
client_ip |
Source IP address |
auth_level |
admin, readonly, local, rejected |
user_agent |
User-Agent header value |
/var/lib/buncker/
├── oci-layout
├── index.json
├── blobs/sha256/
│ ├── a1b2c3d4e5...
│ └── ...
├── meta/sha256/
│ ├── a1b2c3d4e5...json
│ └── ...
├── manifests/{registry}/{repo}/{tag}/{platform}.json
└── logs/buncker.jsonl
{
"source_id": "buncker-prod-01",
"bind": "127.0.0.1",
"port": 5000,
"store_path": "/var/lib/buncker",
"max_workers": 16,
"tls": false,
"crypto": { "salt": "base64...", "mnemonic_hash": "sha256:..." },
"api": { "enabled": false },
"private_registries": ["registry.internal", "localhost:*"],
"gc": { "inactive_days_threshold": 90 },
"log_level": "INFO"
}When api.enabled: true (after buncker api-setup), tls is also set to true.
{
"readonly": "hex-encoded-256-bit-token",
"admin": "hex-encoded-256-bit-token"
}This file only exists after buncker api-setup. It is never readable by non-root users.
~/.buncker/
├── config.json
├── cache/blobs/sha256/
└── logs/fetch.jsonl
buncker/
├── README.md
├── LICENSE
├── Makefile
├── shared/
│ ├── __init__.py
│ ├── crypto.py
│ ├── oci.py
│ └── wordlist.py
├── buncker/
│ ├── __init__.py
│ ├── __main__.py
│ ├── server.py
│ ├── handler.py
│ ├── auth.py
│ ├── resolver.py
│ ├── registry_client.py
│ ├── store.py
│ ├── transfer.py
│ └── config.py
├── buncker_fetch/
│ ├── __init__.py
│ ├── __main__.py
│ ├── fetcher.py
│ ├── registry_client.py
│ ├── cache.py
│ ├── transfer.py
│ └── config.py
├── tests/
│ ├── conftest.py
│ ├── shared/
│ │ ├── test_crypto.py
│ │ └── test_oci.py
│ ├── buncker/
│ │ ├── test_resolver.py
│ │ ├── test_store.py
│ │ ├── test_handler.py
│ │ ├── test_auth.py
│ │ ├── test_transfer.py
│ │ └── test_server_integration.py
│ └── buncker_fetch/
│ ├── test_fetcher.py
│ ├── test_registry_client.py
│ ├── test_cache.py
│ └── test_transfer.py
├── packaging/
│ ├── buncker/debian/
│ │ ├── control
│ │ ├── rules
│ │ ├── install
│ │ ├── buncker.service
│ │ ├── postinst
│ │ └── conffiles
│ └── buncker-fetch/debian/
│ ├── control
│ ├── rules
│ └── install
├── pyproject.toml
└── .gitignore
- Strategy: Manual .deb installation (air-gap requirement)
- CI/CD: GitHub Actions - lint → test → build .deb → artifacts
- Auto-update: request.json.enc includes
buncker_version. buncker-fetch includes newer .deb in response.tar.enc if available. Manual install by operator.
- Dev: Local machine,
python3 -m buncker, store in/tmp/buncker-dev/ - CI: GitHub Actions, Ubuntu latest
- Production offline: Debian 12+ dedicated machine, systemd service
- Production online: Operator workstation, Debian 12+
[Unit]
Description=Buncker - Offline Docker Registry
After=network.target
[Service]
Type=simple
User=buncker
Group=buncker
ExecStart=/usr/bin/buncker serve
Restart=on-failure
RestartSec=5
WorkingDirectory=/var/lib/buncker
NoNewPrivileges=yes
ProtectSystem=strict
ProtectHome=yes
ReadWritePaths=/var/lib/buncker /var/log/buncker
PrivateTmp=yes
[Install]
WantedBy=multi-user.targetdpkg -i buncker_<previous_version>.deb. Store persists across upgrades. Config protected via conffiles.
class BunckerError(Exception): ...
class ConfigError(BunckerError): ...
class CryptoError(BunckerError): ...
class StoreError(BunckerError): ...
class ResolverError(BunckerError): ...
class RegistryError(BunckerError): ...
class TransferError(BunckerError): ...- Atomic writes: temp file + SHA256 verify + rename. Never corrupt the store.
- Actionable errors: Every error message includes what failed, context, and what to do.
- Retry policy (online): 3 attempts, exponential backoff (1s, 3s, 9s). Connect 30s, read 120s.
- Partial import: Valid blobs are kept. Failed blobs are reported. Operator can retry.
- Idempotent: Importing the same blob twice = noop (same digest = same file).
- Format: JSON Lines, append-only
- Levels: DEBUG, INFO, WARNING, ERROR
- Events:
dockerfile_analyzed,transfer_manifest_generated,transfer_imported,blob_pulled,blob_missing,gc_candidate,gc_executed,key_rotation,api_auth_rejected,api_token_reset,api_setup_completed - V2 fields on API requests:
client_ip,auth_level(admin,readonly,local,rejected),user_agent - Never log: mnemonic, derived keys, Bearer tokens, passwords
- Language: Python >=3.11
- Linting: ruff (
E,F,W,I,UP,B,SIM) - Tests: pytest,
tests/mirroring source structure
- No pip, no venv: Only stdlib +
python3-cryptography. Whitelist of allowed imports enforced. - Atomic writes only: All store writes via temp + verify + rename.
- SHA256 verify on every blob read/write: No exceptions, no skip.
- No secrets in logs: Never log mnemonic, keys, tokens.
- Errors must be actionable: What failed + context + what to do.
- No internet fallback (offline): No
urllib.request.urlopeninbuncker/package. Ever. - OCI compliance: Manifests, index, blobs follow OCI Image Spec.
_bunckeris the only allowed extension. - HTTP responses match OCI Distribution Spec: Required headers must be present and correct.
- Type hints on public signatures
@dataclassfor data structures crossing module boundaries- f-strings only (no
%or.format()) pathlib.Pathfor paths (exceptos.renamefor atomic writes)
- Philosophy: Test-after. 80% coverage minimum, 100% on crypto.
- Pyramid: 70% unit, 25% integration, 5% e2e.
- Framework: pytest 8.x,
unittest.mockfor mocking. - Integration: Temp directories, localhost HTTP server, mock OCI registry.
- E2E: Full cycle (setup → analyze → generate → fetch → import → pull) in CI.
- CI Pipeline:
ruff check → ruff format --check → pytest (unit) → pytest (integration) → pytest (e2e) → coverage
- Digest format:
^sha256:[a-f0-9]{64}$ - Tag format:
^[a-zA-Z0-9._-]{1,128}$ - Path traversal prevention on Dockerfile paths (localhost only in V2 - remote sends content, not path)
- No eval, no shell execution of build-args
X-Buncker-Checksumheader validated before any decryption attempt
- BIP-39 mnemonic shared once via human channel
- Derives AES + HMAC keys for USB transfer encryption
- Unchanged in V2
- Activated by
buncker api-setup(optional) - Two cryptographically random tokens (256-bit,
secrets.token_hex(32))- Read-only:
status,logs,gc/report - Admin: all
/admin/*endpoints
- Read-only:
- Token comparison: constant-time (
hmac.compare_digest) - Tokens stored in
/etc/buncker/api-tokens.json(mode 0600) - Token management:
buncker api-show|api-reset readonly|admin(local CLI only) - Failed auth logged with
auth_level: rejected- no information leakage about token validity
- Always unauthenticated (
/v2/*) - Docker clients pull without tokens - Online: registry credentials via env vars only, never plaintext in config
- Mnemonic: communicated once via human channel, encrypted at rest in
/etc/buncker/envusing a key derived from/etc/machine-id(AES-256-GCM, PBKDF2 with 100,000 iterations). Protects against disk theft and unencrypted backups - Derived keys: in-memory only during execution, never written to disk
- Config stores only verification hashes and salts
- API tokens: separate from mnemonic, stored in restricted file (0600), not derived from mnemonic
- Logs NEVER contain: mnemonic, derived keys, Bearer tokens, passwords
- Transfer files (USB): AES-256-GCM + HMAC-SHA256, always encrypted
- LAN: TLS mandatory when API auth is enabled. HTTP allowed only without auth (local-only usage)
- Internet (buncker-fetch): HTTPS mandatory
- Store blobs: cleartext on disk (disk encryption is OS responsibility)
- TLS: operator-provided certificate (internal/external CA) or auto-signed with explicit security warning
- Single dependency:
python3-cryptography(Debian-maintained) - Any new dependency requires explicit justification + Debian package availability
- Product Owner review of this architecture document
- Story creation - Use
/pmthen/create-next-storyto build an ordered backlog - Implementation order suggestion:
- Epic 1: shared/crypto + shared/oci (foundation)
- Epic 2: buncker/store (core storage)
- Epic 3: buncker/resolver (Dockerfile parsing)
- Epic 4: buncker/server + handler (daemon HTTP)
- Epic 5: buncker/transfer (request generation + response import)
- Epic 6: buncker-fetch (online CLI, complete)
- Epic 7: packaging (.deb + systemd + CI)
- Epic 8: e2e tests + documentation
- No frontend architecture needed - both components are CLI/daemon only