Public Access
Initial implementation: aptly container image, Compose stacks, Helm chart, and Gitea Actions pipelines
Provides a self-contained, containerized aptly (Debian repo manager) stack with independently releasable image and Helm chart versions. - images/: aptly-server (aptly built from source, cross-compiled) and aptly-deb-builder (nfpm + dpkg-buildpackage) container images - rootfs/: shared aptly-init/aptly-reconcile/aptly-push/aptly-pack scripts consumed identically by Compose and the Helm chart, driven by one declarative state.yaml contract - compose/: test (ephemeral, open) and production docker-compose stacks with an nginx read/auth sidecar - charts/aptly/: aptly-native Helm chart covering every security posture from fully open to authenticated read+write, Ingress and Gateway API support (usable in parallel for migration scenarios), metrics, and declarative repo/mirror/publish reconciliation via a Helm hook - .gitea/workflows/: CI (lint, template, kubeconform, E2E smoke test) plus separately tagged image (image/v*) and chart (chart/v*) releases, weekly rebuilds, and a preflight workflow validating the runner's Docker/Helm-OCI capabilities - pubkeys/: RSA chart-signing key for Helm --sign / Artifact Hub's signKey annotation (Helm can't verify Ed25519 keys) - docs/, README.md, charts/aptly/README.md: usage, security, and versioning documentation
This commit is contained in:
@@ -0,0 +1,38 @@
|
||||
# Copy to .env and edit. See docs/quickstart-compose.md for the full walkthrough.
|
||||
|
||||
# Image tag to run — set this to a released image/vX.Y.Z-N tag once you have
|
||||
# one (see docs/versioning.md); "latest" is fine to try things out with.
|
||||
APTLY_IMAGE_TAG=latest
|
||||
|
||||
# Host port nginx (reads + the auth-gated /api/) is published on.
|
||||
APTLY_PUBLISH_PORT=8080
|
||||
|
||||
# --- Auth (security.preset "publicRead" equivalent) ---
|
||||
# Password for the internal user aptly-reconcile/aptly-mirror-refresh use to
|
||||
# talk through nginx. Required — has no default, compose refuses to start
|
||||
# without it. Any non-trivial value; nothing external ever needs to know it.
|
||||
APTLY_INTERNAL_PASSWORD=changeme-generate-a-real-secret
|
||||
|
||||
# Also edit config/users (copy from config/users.example) with the
|
||||
# username:password pairs that external CI/uploaders should use.
|
||||
|
||||
# --- GPG signing ---
|
||||
# true (default): publishing requires a signing key at config/gpg/private.asc
|
||||
# (+ config/gpg/passphrase if it's passphrase-protected). Missing key ->
|
||||
# aptly-init logs a WARN and publishes unsigned instead of failing to start
|
||||
# — check `docker compose logs aptly-init` after first boot.
|
||||
# false: explicitly unsigned, no key needed. See docs/security.md.
|
||||
APTLY_GPG_ENABLED=true
|
||||
|
||||
# Directory containing private.asc (and optionally passphrase). Defaults to
|
||||
# ./config/gpg, gitignored.
|
||||
#APTLY_GPG_DIR=./config/gpg
|
||||
|
||||
# --- Reconcile ---
|
||||
# false (default): an unreachable mirror or malformed state.yaml entry only
|
||||
# warns — `docker compose up` still succeeds. Set true in CI to catch
|
||||
# mistakes in state.yaml.
|
||||
APTLY_RECONCILE_FAIL_ON_ERROR=false
|
||||
|
||||
# --- Backup (docker compose --profile backup run --rm backup) ---
|
||||
#APTLY_BACKUP_DIR=./backup
|
||||
@@ -0,0 +1,16 @@
|
||||
# Source aptly config, rendered by aptly-init into /run/aptly/aptly.yaml.
|
||||
# ${VAR}-style placeholders are resolved against the aptly container's
|
||||
# environment at startup — see .env.example for what to set.
|
||||
#
|
||||
# This one file is shared by both docker-compose.test.yaml and
|
||||
# docker-compose.yaml (production); the security posture (auth, network
|
||||
# exposure) is entirely an nginx concern (see nginx.test.conf / nginx.prod.conf),
|
||||
# not an aptly config concern — aptly itself never changes between the two.
|
||||
root_dir: /var/lib/aptly
|
||||
log_level: info
|
||||
gpg_provider: internal
|
||||
serve_in_api_mode: false
|
||||
filesystem_publish_endpoints:
|
||||
public:
|
||||
root_dir: /var/lib/aptly/public
|
||||
link_method: hardlink
|
||||
@@ -0,0 +1,48 @@
|
||||
# Production topology: security.preset "publicRead" equivalent — reading the
|
||||
# repo (apt clients) needs no credentials, writing (the API under /api/) does.
|
||||
# Health-check paths stay exempt so container/orchestrator probes never need
|
||||
# credentials in any mode.
|
||||
#
|
||||
# Mounted at /etc/nginx/conf.d/default.conf, which the base image's own
|
||||
# nginx.conf already `include`s from inside its own http{} block — so this
|
||||
# file must contain ONLY a server{} block (or other http-context directives),
|
||||
# never its own http{}/events{}/worker_processes wrapper.
|
||||
server {
|
||||
listen 8080;
|
||||
server_name _;
|
||||
client_max_body_size 0;
|
||||
absolute_redirect off;
|
||||
|
||||
location = /healthz { access_log off; return 200 "ok\n"; }
|
||||
|
||||
location = /api/ready { access_log off; proxy_pass http://aptly:8080; }
|
||||
location = /api/healthy { access_log off; proxy_pass http://aptly:8080; }
|
||||
|
||||
location /api/ {
|
||||
auth_basic "aptly";
|
||||
auth_basic_user_file /run/aptly/htpasswd;
|
||||
|
||||
proxy_pass http://aptly:8080;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_request_buffering off;
|
||||
proxy_read_timeout 3600s;
|
||||
proxy_send_timeout 3600s;
|
||||
}
|
||||
|
||||
location = /signing-key.asc {
|
||||
alias /run/aptly/pub/signing-key.asc;
|
||||
default_type text/plain;
|
||||
}
|
||||
|
||||
location / {
|
||||
root /var/lib/aptly/public;
|
||||
autoindex on;
|
||||
autoindex_exact_size off;
|
||||
|
||||
location ~* /(InRelease|Release|Release\.gpg|Packages(\.[a-z0-9]+)?|Sources(\.[a-z0-9]+)?)$ {
|
||||
root /var/lib/aptly/public;
|
||||
add_header Cache-Control "no-cache" always;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
# Test topology: everything open (security.preset "open" equivalent) — no
|
||||
# auth on read OR write. This is what "komplett unabgesichert" looks like.
|
||||
# Do not reuse this file for docker-compose.yaml (production).
|
||||
#
|
||||
# Mounted at /etc/nginx/conf.d/default.conf, which the base image's own
|
||||
# nginx.conf already `include`s from inside its own http{} block — so this
|
||||
# file must contain ONLY a server{} block (or other http-context directives),
|
||||
# never its own http{}/events{}/worker_processes wrapper.
|
||||
server {
|
||||
listen 8080;
|
||||
server_name _;
|
||||
client_max_body_size 0;
|
||||
absolute_redirect off;
|
||||
|
||||
location = /healthz { access_log off; return 200 "ok\n"; }
|
||||
|
||||
location = /api/ready { access_log off; proxy_pass http://aptly:8080; }
|
||||
location = /api/healthy { access_log off; proxy_pass http://aptly:8080; }
|
||||
|
||||
location /api/ {
|
||||
proxy_pass http://aptly:8080;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_request_buffering off;
|
||||
proxy_read_timeout 3600s;
|
||||
proxy_send_timeout 3600s;
|
||||
}
|
||||
|
||||
location = /signing-key.asc {
|
||||
alias /run/aptly/pub/signing-key.asc;
|
||||
default_type text/plain;
|
||||
}
|
||||
|
||||
location / {
|
||||
root /var/lib/aptly/public;
|
||||
autoindex on;
|
||||
autoindex_exact_size off;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
# Demo state for docker-compose.test.yaml: one local repo, published at the
|
||||
# repo root, completely open and unsigned. Not meant for production — see
|
||||
# compose/config/state.yaml for the starter template used there.
|
||||
localRepos:
|
||||
- name: demo
|
||||
comment: "docker-compose.test.yaml demo repo"
|
||||
defaultDistribution: stable
|
||||
defaultComponent: main
|
||||
|
||||
publish:
|
||||
- name: demo-root
|
||||
prefix: ""
|
||||
distribution: stable
|
||||
sourceKind: local
|
||||
sources:
|
||||
- { name: demo, component: main }
|
||||
architectures: [amd64, arm64]
|
||||
acquireByHash: true
|
||||
@@ -0,0 +1,31 @@
|
||||
# Declarative aptly state for docker-compose.yaml (production). Consumed by
|
||||
# aptly-reconcile on every start of the `reconcile` service — safe to edit and
|
||||
# restart that service as often as you like, it only converges towards
|
||||
# whatever is described here (see docs/packaging.md).
|
||||
#
|
||||
# Uncomment and adjust the example below to get your first repo + publish
|
||||
# target, or add your own. See rootfs/usr/local/bin/aptly-reconcile for the
|
||||
# full field reference.
|
||||
|
||||
localRepos: []
|
||||
# - name: stable
|
||||
# comment: "Production package repository"
|
||||
# defaultDistribution: stable
|
||||
# defaultComponent: main
|
||||
|
||||
mirrors: []
|
||||
# - name: debian-security
|
||||
# archiveURL: http://security.debian.org/debian-security
|
||||
# distribution: trixie-security
|
||||
# components: [main]
|
||||
# architectures: [amd64, arm64]
|
||||
|
||||
publish: []
|
||||
# - name: stable-root
|
||||
# prefix: ""
|
||||
# distribution: stable
|
||||
# sourceKind: local
|
||||
# sources:
|
||||
# - { name: stable, component: main }
|
||||
# architectures: [amd64, arm64]
|
||||
# acquireByHash: true
|
||||
@@ -0,0 +1,7 @@
|
||||
# Plaintext "user:password" lines, one per line — hashed into htpasswd by
|
||||
# aptly-init at container start (never store a bcrypt/apr1 hash here yourself;
|
||||
# see rootfs/usr/local/bin/aptly-init for why). Copy this file to
|
||||
# compose/config/users (gitignored) and edit it before starting
|
||||
# docker-compose.yaml (production). Blank lines and anything without a colon
|
||||
# are ignored.
|
||||
ci:change-me-please
|
||||
@@ -0,0 +1,75 @@
|
||||
# Test stack: fully open (no auth on read OR write), unsigned, ephemeral
|
||||
# volumes, ready to use with a single `docker compose -f compose/docker-compose.test.yaml up`.
|
||||
# This is also what tests/smoke-test.sh drives — see docs/quickstart-compose.md.
|
||||
#
|
||||
# Topology mirrors production (aptly-init -> aptly -> nginx, aptly never
|
||||
# reachable from outside the compose network) so that what you test here is
|
||||
# what you'd actually run, just with the security matrix dialed to "open".
|
||||
services:
|
||||
aptly-init:
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: images/aptly-server/Dockerfile
|
||||
entrypoint: ["/usr/local/bin/aptly-init"]
|
||||
environment:
|
||||
APTLY_GPG_ENABLED: "false"
|
||||
volumes:
|
||||
- ./config/aptly.yaml:/etc/aptly-src/aptly.yaml:ro
|
||||
- aptly_run:/run/aptly
|
||||
restart: "no"
|
||||
|
||||
aptly:
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: images/aptly-server/Dockerfile
|
||||
depends_on:
|
||||
aptly-init:
|
||||
condition: service_completed_successfully
|
||||
environment:
|
||||
APTLY_API_LISTEN: "0.0.0.0:8080"
|
||||
volumes:
|
||||
- aptly_data:/var/lib/aptly
|
||||
- aptly_run:/run/aptly
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-fsS", "http://127.0.0.1:8080/api/healthy"]
|
||||
interval: 5s
|
||||
timeout: 3s
|
||||
retries: 20
|
||||
start_period: 5s
|
||||
|
||||
nginx:
|
||||
image: nginxinc/nginx-unprivileged:1-alpine
|
||||
depends_on:
|
||||
aptly:
|
||||
condition: service_healthy
|
||||
volumes:
|
||||
- aptly_data:/var/lib/aptly:ro
|
||||
- aptly_run:/run/aptly:ro
|
||||
- ./config/nginx.test.conf:/etc/nginx/conf.d/default.conf:ro
|
||||
ports:
|
||||
- "8080:8080"
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "-qO-", "http://127.0.0.1:8080/healthz"]
|
||||
interval: 5s
|
||||
timeout: 3s
|
||||
retries: 20
|
||||
|
||||
reconcile:
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: images/aptly-server/Dockerfile
|
||||
depends_on:
|
||||
aptly:
|
||||
condition: service_healthy
|
||||
entrypoint: ["/usr/local/bin/aptly-reconcile"]
|
||||
environment:
|
||||
APTLY_URL: "http://aptly:8080"
|
||||
APTLY_STATE_FILE: "/state.yaml"
|
||||
APTLY_FAIL_ON_ERROR: "true"
|
||||
volumes:
|
||||
- ./config/state.test.yaml:/state.yaml:ro
|
||||
restart: "no"
|
||||
|
||||
volumes:
|
||||
aptly_data:
|
||||
aptly_run:
|
||||
@@ -0,0 +1,134 @@
|
||||
# Production stack: security.preset "publicRead" equivalent — reads are
|
||||
# open (apt clients need no credentials), the mutating API is behind Basic
|
||||
# Auth. aptly itself is never published to the host; only nginx is. See
|
||||
# docs/security.md for how to move to the other three presets (fully open,
|
||||
# fully authenticated, or read-only), and .env.example for every variable
|
||||
# used below.
|
||||
#
|
||||
# First run:
|
||||
# cp .env.example .env && edit it
|
||||
# cp config/users.example config/users && edit it (at least change the password)
|
||||
# docker compose up -d
|
||||
# docker compose logs -f aptly-init # check for GPG warnings
|
||||
services:
|
||||
aptly-init:
|
||||
image: git.morlana.online/f.weber/aptly:${APTLY_IMAGE_TAG:-latest}
|
||||
entrypoint: ["/usr/local/bin/aptly-init"]
|
||||
environment:
|
||||
APTLY_GPG_ENABLED: "${APTLY_GPG_ENABLED:-true}"
|
||||
# Fixed in-container paths — put your key material at the host paths
|
||||
# below (an empty/missing directory is fine: aptly-init then warns and
|
||||
# publishes unsigned instead of failing to start).
|
||||
APTLY_GPG_PRIVATE_KEY_FILE: "/etc/aptly-secrets/gpg/private.asc"
|
||||
APTLY_GPG_PASSPHRASE_FILE: "/etc/aptly-secrets/gpg/passphrase"
|
||||
APTLY_USERS_FILE: "/etc/aptly-secrets/users"
|
||||
APTLY_INTERNAL_USER: "aptly-internal"
|
||||
APTLY_INTERNAL_PASSWORD: "${APTLY_INTERNAL_PASSWORD:?set APTLY_INTERNAL_PASSWORD in .env}"
|
||||
volumes:
|
||||
- ./config/aptly.yaml:/etc/aptly-src/aptly.yaml:ro
|
||||
- ./config/users:/etc/aptly-secrets/users:ro
|
||||
- ${APTLY_GPG_DIR:-./config/gpg}:/etc/aptly-secrets/gpg:ro
|
||||
- aptly_run:/run/aptly
|
||||
restart: "no"
|
||||
|
||||
aptly:
|
||||
image: git.morlana.online/f.weber/aptly:${APTLY_IMAGE_TAG:-latest}
|
||||
depends_on:
|
||||
aptly-init:
|
||||
condition: service_completed_successfully
|
||||
environment:
|
||||
APTLY_API_LISTEN: "0.0.0.0:8080"
|
||||
volumes:
|
||||
- aptly_data:/var/lib/aptly
|
||||
- aptly_run:/run/aptly
|
||||
read_only: true
|
||||
tmpfs:
|
||||
- /tmp
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-fsS", "http://127.0.0.1:8080/api/healthy"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 20
|
||||
start_period: 10s
|
||||
restart: unless-stopped
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
memory: 1g
|
||||
logging:
|
||||
driver: json-file
|
||||
options:
|
||||
max-size: "10m"
|
||||
max-file: "3"
|
||||
|
||||
nginx:
|
||||
image: nginxinc/nginx-unprivileged:1-alpine
|
||||
depends_on:
|
||||
aptly:
|
||||
condition: service_healthy
|
||||
volumes:
|
||||
- aptly_data:/var/lib/aptly:ro
|
||||
- aptly_run:/run/aptly:ro
|
||||
- ./config/nginx.prod.conf:/etc/nginx/conf.d/default.conf:ro
|
||||
ports:
|
||||
- "${APTLY_PUBLISH_PORT:-8080}:8080"
|
||||
read_only: true
|
||||
tmpfs:
|
||||
- /tmp
|
||||
- /var/cache/nginx
|
||||
- /run
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "-qO-", "http://127.0.0.1:8080/healthz"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 20
|
||||
restart: unless-stopped
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
memory: 256m
|
||||
logging:
|
||||
driver: json-file
|
||||
options:
|
||||
max-size: "10m"
|
||||
max-file: "3"
|
||||
# --- TLS via an external reverse proxy (recommended) ---
|
||||
# Put Traefik/Caddy/whatever you already run in front of this service
|
||||
# instead of terminating TLS here. Traefik label example:
|
||||
# labels:
|
||||
# - "traefik.enable=true"
|
||||
# - "traefik.http.routers.aptly.rule=Host(`apt.example.com`)"
|
||||
# - "traefik.http.routers.aptly.tls.certresolver=letsencrypt"
|
||||
|
||||
reconcile:
|
||||
image: git.morlana.online/f.weber/aptly:${APTLY_IMAGE_TAG:-latest}
|
||||
depends_on:
|
||||
aptly:
|
||||
condition: service_healthy
|
||||
entrypoint: ["/usr/local/bin/aptly-reconcile"]
|
||||
environment:
|
||||
APTLY_URL: "http://aptly:8080"
|
||||
APTLY_STATE_FILE: "/state.yaml"
|
||||
APTLY_FAIL_ON_ERROR: "${APTLY_RECONCILE_FAIL_ON_ERROR:-false}"
|
||||
volumes:
|
||||
- ./config/state.yaml:/state.yaml:ro
|
||||
restart: "no"
|
||||
|
||||
# Run on demand: docker compose --profile backup run --rm backup
|
||||
backup:
|
||||
image: git.morlana.online/f.weber/aptly:${APTLY_IMAGE_TAG:-latest}
|
||||
profiles: ["backup"]
|
||||
entrypoint: ["/bin/sh", "-c"]
|
||||
command:
|
||||
- >
|
||||
set -eu;
|
||||
ts=$$(date -u +%Y%m%dT%H%M%SZ);
|
||||
tar -C /var/lib/aptly --exclude=.gnupg -c . | zstd -q -o "/backup/aptly-$${ts}.tar.zst";
|
||||
echo "wrote /backup/aptly-$${ts}.tar.zst"
|
||||
volumes:
|
||||
- aptly_data:/var/lib/aptly:ro
|
||||
- ${APTLY_BACKUP_DIR:-./backup}:/backup
|
||||
|
||||
volumes:
|
||||
aptly_data:
|
||||
aptly_run:
|
||||
Reference in New Issue
Block a user