diff --git a/config.tfvars b/config.tfvars index 804ef7b8..9ec90c1f 100644 Binary files a/config.tfvars and b/config.tfvars differ diff --git a/modules/docker-registry/config-private.yml b/modules/docker-registry/config-private.yml index f8a188e3..29453355 100644 --- a/modules/docker-registry/config-private.yml +++ b/modules/docker-registry/config-private.yml @@ -16,6 +16,10 @@ storage: age: 168h interval: 4h dryrun: false +auth: + htpasswd: + realm: "Registry Realm" + path: /auth/htpasswd http: addr: :5000 headers: diff --git a/modules/docker-registry/docker-compose.yml b/modules/docker-registry/docker-compose.yml index d078639a..4b3d39a8 100644 --- a/modules/docker-registry/docker-compose.yml +++ b/modules/docker-registry/docker-compose.yml @@ -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 diff --git a/stacks/infra/main.tf b/stacks/infra/main.tf index 8fd1d899..a27778c2 100644 --- a/stacks/infra/main.tf +++ b/stacks/infra/main.tf @@ -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")) diff --git a/stacks/kyverno/modules/kyverno/registry-credentials.tf b/stacks/kyverno/modules/kyverno/registry-credentials.tf new file mode 100644 index 00000000..feded5b3 --- /dev/null +++ b/stacks/kyverno/modules/kyverno/registry-credentials.tf @@ -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, + ] +} diff --git a/stacks/reverse-proxy/modules/reverse_proxy/main.tf b/stacks/reverse-proxy/modules/reverse_proxy/main.tf index e6dcc34b..c30aa01c 100644 --- a/stacks/reverse-proxy/modules/reverse_proxy/main.tf +++ b/stacks/reverse-proxy/modules/reverse_proxy/main.tf @@ -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"