Files
aptly-containerized/charts/aptly/values.yaml
T
f.weber 103ad311b7
CI / lint (push) Failing after 24s
CI / smoke-test (push) Failing after 2m4s
Release image / release (push) Successful in 23m18s
Release chart / release (push) Successful in 7s
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
2026-08-12 12:21:08 +02:00

323 lines
11 KiB
YAML

nameOverride: ""
fullnameOverride: ""
# -- Container image for the aptly server itself (also used for the
# initContainer and the reconcile Job — all three run the same image).
image:
repository: git.morlana.online/f.weber/aptly
tag: "" # "" -> .Chart.AppVersion, i.e. the last released image. Never "latest".
pullPolicy: IfNotPresent
pullSecrets: []
# -- The read/auth sidecar. A plain upstream image — this chart owns none of
# its code, only its rendered config (see `security` below).
nginx:
image:
repository: nginxinc/nginx-unprivileged
tag: "1-alpine"
pullPolicy: IfNotPresent
resources: {}
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities: { drop: [ALL] }
# =============================================================================
# aptly configuration. Two layers, always merged in this order:
# 1. the curated keys below (omitted from the rendered config when unset,
# so a default install matches aptly's own upstream defaults exactly)
# 2. aptly.configOverrides — raw aptly YAML, deep-merged last, always wins.
# Every current and future aptly config key is reachable here without a
# chart change. See rootfs/usr/local/bin/aptly-init and
# https://github.com/aptly-dev/aptly/blob/master/utils/config.go for the
# full field list (snake_case yaml tags).
# =============================================================================
aptly:
architectures: []
logLevel: info
logFormat: json
download:
concurrency: 4
limit: 0
retries: 0
sourcePackages: false
publishing:
skipContents: false
skipBz2: false
metrics:
enabled: false
swagger:
enabled: false
gpg:
# false -> gpg_disable_sign: true AND Signing.Skip: true on every publish
# call the reconcile Job makes (both are required — aptly's publish API
# does not consult gpg_disable_sign on its own, see docs/security.md).
enabled: true
verify: true
# gpg (default): the real gnupg binary, in the image already — handles
# armored keys, subkeys and passphrases the way upstream aptly expects.
# internal: pure-Go openpgp, no gnupg binary needed, smaller attack
# surface — verify your key type works with it before switching.
provider: gpg
signingKey:
# Secret with key "privateKey" (armored .asc) or "secretKeyring"
# (binary secring.gpg), optionally "passphrase". Never put a real key
# inline in values.yaml — this is the production path.
existingSecret: ""
# Discouraged escape hatch for quick tests only.
privateKey: ""
passphrase: ""
publishPublicKey:
enabled: true
path: /signing-key.asc
# Trusted keys imported into GNUPGHOME on every start, for mirror
# signature verification. The keyring is therefore a pure function of
# values.yaml — restart the pod to pick up an edit here.
# Inline ASCII-armored public keys only (Helm has no network access at
# render time to support a `url:`/`keyserver:` form the way a plain script
# could — fetch the key yourself once and paste it here).
gpgKeys: []
# - name: debian-archive
# armored: |
# -----BEGIN PGP PUBLIC KEY BLOCK-----
# ...
# Declarative desired state, reconciled by the post-install/post-upgrade
# Job against the REST API (never the CLI — see reconcile.mode below and
# rootfs/usr/local/bin/aptly-reconcile for the exact field reference and
# the documented limitation on editing mirrors[].components after creation).
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: "" # "" = repo root
# distribution: stable
# sourceKind: local # local | snapshot
# sources: [{ name: stable, component: main }]
# architectures: [amd64, arm64]
# acquireByHash: true
# Raw passthrough, deep-merged over the generated config last. See header.
configOverrides: {}
# Existing Secrets to envFrom into the aptly container, so configOverrides
# can reference ${VAR} placeholders (e.g. S3 credentials) that resolve from
# Secrets you already manage, without ever putting them in values.yaml.
existingSecretEnv: []
# =============================================================================
# Security matrix — one preset switch, escape hatches for every axis. See
# docs/security.md for the full decision table.
# =============================================================================
security:
# open: read+write, no auth, no exceptions — the explicit "unabgesichert"
# mode. publicRead (default): read is open, write needs Basic Auth.
# authenticated: both need Basic Auth. readOnly: write returns 404.
preset: publicRead
auth:
# name: plaintext password. Hashed into htpasswd by the initContainer at
# pod start (never a bcrypt/apr1 hash here — see rootfs/.../aptly-init
# for why: a template-side hash would change, hence restart-loop, on
# every single helm upgrade).
users: {}
# Secret key "htpasswd" (pre-hashed) — the recommended production path,
# e.g. via ExternalSecrets/SealedSecrets. Wins over `users` when set.
existingSecret: ""
internalUser:
# Always appended to htpasswd: the reconcile Job talks to nginx (not
# directly to aptly, which is loopback-only), so it needs credentials
# in every preset, including existingSecret + authenticated.
enabled: true
username: aptly-internal
# CIDRs matched against $remote_addr. Behind an Ingress controller that is
# the CONTROLLER's pod IP, not the real client — set trustedProxies to the
# controller's CIDR (via X-Forwarded-For) or use networkPolicy instead. The
# chart renders a warning comment into nginx.conf when allowCIDRs is set
# without trustedProxies.
trustedProxies: []
read:
enabled: true
requireAuth: null # null = take the preset's value; true/false overrides it
allowCIDRs: []
write:
enabled: true
requireAuth: null
allowCIDRs: []
# true: do not render the API Ingress at all (regardless of ingress.api.*)
# and rely on networkPolicy for isolation — an honest implementation, not
# an nginx trick.
inClusterOnly: false
# proxy.enabled=false hands aptly's unauthenticated write API directly to
# whatever can reach the Service — the chart refuses to render an Ingress in
# that combination unless security.preset is explicitly "open" (see
# templates/NOTES.txt / the `fail` guard in templates/_helpers.tpl).
proxy:
enabled: true
# nginx additionally serves the same tree under /repos/<name>/, matching
# aptly's own serve_in_api_mode URL shape, so toggling this flag never
# breaks an already-deployed sources.list.
compatPaths: true
maxUploadSize: "0" # nginx client_max_body_size; "0" = unlimited
readTimeout: "3600s"
publishEndpointName: public
persistence:
enabled: true
existingClaim: "" # set this in production — see docs/operations.md
storageClass: ""
accessMode: ReadWriteOnce
size: 20Gi
annotations: {}
workload:
updateStrategy:
type: RollingUpdate # safe here: a StatefulSet with replicas=1 always
# terminates the old pod before creating the new one
podManagementPolicy: OrderedReady
revisionHistoryLimit: 3
terminationGracePeriodSeconds: 60
annotations: {}
podAnnotations: {}
podLabels: {}
podSecurityContext:
runAsNonRoot: true
runAsUser: 10001
runAsGroup: 10001
fsGroup: 10001
fsGroupChangePolicy: OnRootMismatch
seccompProfile: { type: RuntimeDefault }
containerSecurityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities: { drop: [ALL] }
resources: {}
probes:
startup: { periodSeconds: 5, failureThreshold: 60 }
readiness: { periodSeconds: 10, timeoutSeconds: 3, failureThreshold: 3 }
liveness: { periodSeconds: 30, timeoutSeconds: 5, failureThreshold: 6 }
service:
type: ClusterIP
port: 8080
annotations: {}
ingress:
enabled: false
mode: single # single | split — see docs/security.md
className: ""
annotations:
nginx.ingress.kubernetes.io/proxy-body-size: "0"
nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
repo:
host: ""
path: /
pathType: Prefix
tls: []
# - hosts: [apt.example.com]
# secretName: apt-tls
api:
enabled: false
host: ""
className: ""
annotations: {}
tls: []
# Gateway API HTTPRoute support — fully independent of `ingress.*` above, and
# safe to enable at the same time as it (e.g. mid-migration between the two:
# both can point at the same Service simultaneously, see docs/security.md).
# This chart never creates a Gateway itself, only HTTPRoutes attaching to one
# your cluster admin already manages — TLS is that Gateway listener's job, not
# something set here.
gateway:
enabled: false
# Core Gateway API resources are apiVersion gateway.networking.k8s.io/v1 (GA
# since v1.0) — override only if your cluster's CRDs are still pre-GA.
apiVersion: gateway.networking.k8s.io/v1
mode: single # single | split — same meaning as ingress.mode, see docs/security.md
# Referenced Gateway(s). Required when gateway.enabled is true.
parentRefs: []
# - name: my-gateway
# namespace: gateway-infra # optional, defaults to this release's namespace
# sectionName: https # optional, binds to one named listener
repo:
hostnames: [] # e.g. [apt.example.com] — omit to match the Gateway listener's own hostname(s)
path: /
pathType: PathPrefix # PathPrefix | Exact | RegularExpression — Gateway API's own enum, distinct from ingress.repo.pathType's
api:
enabled: false
hostnames: []
parentRefs: [] # override for the API route only — falls back to gateway.parentRefs when empty
metrics:
service:
enabled: false
port: 9090
annotations: {}
serviceMonitor:
enabled: false
interval: 30s
labels: {}
relabelings: []
reconcile:
enabled: true
# hook (default): post-install,post-upgrade Helm hook Job.
# job: a plain Job named with a hash of the desired state, for GitOps
# controllers (ArgoCD/Flux) that dislike Helm hooks.
# manual: render the state ConfigMap only.
mode: hook
failOnError: false
timeoutSeconds: 600
image: {} # override repository/tag/pullPolicy; defaults to the main `image`
resources: {}
podDisruptionBudget:
enabled: false
maxUnavailable: 1
networkPolicy:
enabled: false
allowedNamespaces: []
extraIngress: []
egress:
# A default-deny egress policy silently breaks every mirror — this stays
# true until you have a specific reason to lock it down.
allowAll: true
extra: []
extraEnv: []
extraEnvFrom: []
extraVolumes: []
extraVolumeMounts: []
extraInitContainers: []
extraContainers: []
nodeSelector: {}
tolerations: []
affinity: {}
topologySpreadConstraints: []
priorityClassName: ""
global:
imageRegistry: ""
imagePullSecrets: []
defaultStorageClass: ""