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,322 @@
|
||||
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: ""
|
||||
Reference in New Issue
Block a user