add htpasswd auth to private docker registry + expose at registry.viktorbarzin.me

- Add auth.htpasswd section to config-private.yml
- Mount htpasswd file in registry-private container, fix healthcheck for 401
- Rename registry UI from registry.viktorbarzin.me → docker.viktorbarzin.me
- Add Docker CLI ingress at registry.viktorbarzin.me (HTTPS backend, no rate-limit, unlimited body)
- Add docker to cloudflare_proxied_names (registry stays non-proxied)
- Add Kyverno ClusterPolicy to sync registry-credentials secret to all namespaces
- Update infra provisioning to install apache2-utils and generate htpasswd from Vault
This commit is contained in:
Viktor Barzin 2026-03-22 22:10:10 +02:00
parent e4f478b490
commit 36171bcda4
6 changed files with 123 additions and 5 deletions

Binary file not shown.

View file

@ -16,6 +16,10 @@ storage:
age: 168h
interval: 4h
dryrun: false
auth:
htpasswd:
realm: "Registry Realm"
path: /auth/htpasswd
http:
addr: :5000
headers:

View file

@ -92,10 +92,12 @@ services:
volumes:
- /opt/registry/data/private:/var/lib/registry
- /opt/registry/config-private.yml:/etc/docker/registry/config.yml:ro
- /opt/registry/htpasswd:/auth/htpasswd:ro
networks:
- registry
healthcheck:
test: ["CMD", "sh", "-c", "wget -qO- http://localhost:5000/v2/ >/dev/null 2>&1"]
# 401 is expected (auth required) — any HTTP response means the registry is healthy
test: ["CMD", "sh", "-c", "wget -qS -O /dev/null http://localhost:5000/v2/ 2>&1 | grep -q 'HTTP/'"]
interval: 30s
timeout: 10s
retries: 3

View file

@ -21,6 +21,11 @@ data "vault_kv_secret_v2" "secrets" {
name = "infra"
}
data "vault_kv_secret_v2" "viktor" {
mount = "secret"
name = "viktor"
}
# ---------------------------------------------------------------------------
# Locals
# ---------------------------------------------------------------------------
@ -176,8 +181,8 @@ module "docker-registry-template" {
# Setup registry config and start container
provision_cmds = [
# Install and enable QEMU guest agent for remote management
"apt-get install -y qemu-guest-agent",
# Install dependencies (QEMU guest agent + htpasswd for registry auth)
"apt-get install -y qemu-guest-agent apache2-utils",
"systemctl enable qemu-guest-agent",
"systemctl start qemu-guest-agent",
# Stop host nginx we run nginx inside Docker instead
@ -185,6 +190,11 @@ module "docker-registry-template" {
"systemctl disable nginx || true",
# Create directory structure
"mkdir -p /opt/registry/data/dockerhub /opt/registry/data/ghcr /opt/registry/data/quay /opt/registry/data/k8s /opt/registry/data/kyverno /opt/registry/data/private /opt/registry/tls",
# Generate htpasswd file for private registry authentication
format("htpasswd -Bbn %s %s > /opt/registry/htpasswd",
data.vault_kv_secret_v2.viktor.data["registry_user"],
data.vault_kv_secret_v2.viktor.data["registry_password"]
),
# Write Docker Compose file
format("echo %s | base64 -d > /opt/registry/docker-compose.yml",
base64encode(file("${path.root}/../../modules/docker-registry/docker-compose.yml"))

View file

@ -0,0 +1,83 @@
# =============================================================================
# Private Docker Registry Credentials Auto-sync to all namespaces
# =============================================================================
# Source secret in kyverno namespace, cloned by ClusterPolicy into every NS.
# Pods use imagePullSecrets: [{name: registry-credentials}] to pull from
# registry.viktorbarzin.me (or 10.0.20.10:5050 internally).
data "vault_kv_secret_v2" "viktor" {
mount = "secret"
name = "viktor"
}
resource "kubernetes_secret" "registry_credentials" {
metadata {
name = "registry-credentials"
namespace = kubernetes_namespace.kyverno.metadata[0].name
}
type = "kubernetes.io/dockerconfigjson"
data = {
".dockerconfigjson" = jsonencode({
auths = {
"registry.viktorbarzin.me" = {
auth = base64encode("${data.vault_kv_secret_v2.viktor.data["registry_user"]}:${data.vault_kv_secret_v2.viktor.data["registry_password"]}")
}
"10.0.20.10:5050" = {
auth = base64encode("${data.vault_kv_secret_v2.viktor.data["registry_user"]}:${data.vault_kv_secret_v2.viktor.data["registry_password"]}")
}
}
})
}
}
resource "kubernetes_manifest" "sync_registry_credentials" {
manifest = {
apiVersion = "kyverno.io/v1"
kind = "ClusterPolicy"
metadata = {
name = "sync-registry-credentials"
}
spec = {
rules = [
{
name = "sync-registry-secret"
match = {
any = [
{
resources = {
kinds = ["Namespace"]
}
}
]
}
exclude = {
any = [
{
resources = {
namespaces = ["kube-system", "kube-public", "kube-node-lease"]
}
}
]
}
generate = {
apiVersion = "v1"
kind = "Secret"
name = "registry-credentials"
namespace = "{{request.object.metadata.name}}"
synchronize = true
clone = {
namespace = "kyverno"
name = "registry-credentials"
}
}
}
]
}
}
depends_on = [
helm_release.kyverno,
kubernetes_secret.registry_credentials,
]
}

View file

@ -186,10 +186,10 @@ module "proxmox" {
}
}
# https://registry.viktorbarzin.me/
# https://docker.viktorbarzin.me/ (registry web UI)
module "docker-registry-ui" {
source = "./factory"
name = "registry"
name = "docker"
external_name = "docker-registry.viktorbarzin.lan"
port = 8080
tls_secret_name = var.tls_secret_name
@ -206,6 +206,25 @@ module "docker-registry-ui" {
}
}
# https://registry.viktorbarzin.me/ (Docker CLI push/pull endpoint)
module "docker-registry-cli" {
source = "./factory"
name = "registry"
external_name = "docker-registry.viktorbarzin.lan"
port = 5050
backend_protocol = "HTTPS"
tls_secret_name = var.tls_secret_name
protected = false # Docker CLI uses htpasswd, NOT Authentik
max_body_size = "0" # unlimited - Docker layers can be large
depends_on = [kubernetes_namespace.reverse-proxy]
extra_annotations = {
# Skip rate-limit (Docker push/pull generates many rapid requests)
# Keep CrowdSec for L7 protection
"traefik.ingress.kubernetes.io/router.middlewares" = "traefik-csp-headers@kubernetescrd,traefik-crowdsec@kubernetescrd"
"gethomepage.dev/enabled" = "false"
}
}
# https://valchedrym.viktorbarzin.me/
module "valchedrym" {
source = "./factory"