From 054e0e1e8761b97f56a06401f91efe53ebca4533 Mon Sep 17 00:00:00 2001 From: Pieter van Boheemen Date: Mon, 5 Jan 2026 16:40:37 +0100 Subject: [PATCH] Deploy Zitadel identity provider with DNS automation (#3) (#8) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit implements a complete Zitadel identity provider deployment with automated DNS management using vrije.cloud domain. ## Infrastructure Changes ### DNS Management - Migrated from deprecated hetznerdns provider to modern hcloud provider v1.57+ - Automated DNS record creation for client subdomains (test.vrije.cloud) - Automated wildcard DNS for service subdomains (*.test.vrije.cloud) - Supports both IPv4 (A) and IPv6 (AAAA) records ### Zitadel Deployment - Added complete Zitadel role with PostgreSQL 16 database - Configured Zitadel v2.63.7 with proper external domain settings - Implemented first instance setup with admin user creation - Set up database connection with proper user and admin credentials - Configured email verification bypass for first admin user ### Traefik Updates - Upgraded from v3.0 to v3.2 for better Docker API compatibility - Added manual routing configuration in dynamic.yml for Zitadel - Configured HTTP/2 Cleartext (h2c) backend for Zitadel service - Added Zitadel-specific security headers middleware - Fixed Docker API version compatibility issues ### Secrets Management - Added Zitadel credentials to test client secrets - Generated proper 32-character masterkey (Zitadel requirement) - Created admin password with symbol complexity requirement - Added zitadel_domain configuration ## Deployment Details Test environment now accessible at: - Server: test.vrije.cloud (78.47.191.38) - Zitadel: https://zitadel.test.vrije.cloud/ - Admin user: admin@test.zitadel.test.vrije.cloud Successfully tested: - HTTPS with Let's Encrypt SSL certificate - Admin login with 2FA setup - First instance initialization Fixes #3 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Pieter Co-authored-by: Claude --- ansible/playbooks/deploy.yml | 43 ++++++++ ansible/roles/traefik/defaults/main.yml | 4 +- .../traefik/templates/docker-compose.yml.j2 | 3 + .../roles/traefik/templates/dynamic.yml.j2 | 26 +++++ ansible/roles/zitadel/defaults/main.yml | 33 +++++++ ansible/roles/zitadel/handlers/main.yml | 9 ++ ansible/roles/zitadel/tasks/bootstrap.yml | 32 ++++++ ansible/roles/zitadel/tasks/docker.yml | 49 +++++++++ ansible/roles/zitadel/tasks/main.yml | 13 +++ ansible/roles/zitadel/tasks/oidc-apps.yml | 7 ++ .../templates/docker-compose.zitadel.yml.j2 | 99 +++++++++++++++++++ secrets/clients/test.sops.yaml | 49 ++++----- tofu/dns.tf | 69 +++++++------ tofu/main.tf | 5 +- tofu/versions.tf | 8 +- 15 files changed, 383 insertions(+), 66 deletions(-) create mode 100644 ansible/playbooks/deploy.yml create mode 100644 ansible/roles/zitadel/defaults/main.yml create mode 100644 ansible/roles/zitadel/handlers/main.yml create mode 100644 ansible/roles/zitadel/tasks/bootstrap.yml create mode 100644 ansible/roles/zitadel/tasks/docker.yml create mode 100644 ansible/roles/zitadel/tasks/main.yml create mode 100644 ansible/roles/zitadel/tasks/oidc-apps.yml create mode 100644 ansible/roles/zitadel/templates/docker-compose.zitadel.yml.j2 diff --git a/ansible/playbooks/deploy.yml b/ansible/playbooks/deploy.yml new file mode 100644 index 0000000..2f657cb --- /dev/null +++ b/ansible/playbooks/deploy.yml @@ -0,0 +1,43 @@ +--- +# Deploy applications to client servers +# This playbook deploys Zitadel, Nextcloud, and other applications + +- name: Deploy applications to client servers + hosts: all + become: yes + + pre_tasks: + - name: Gather facts + setup: + + - name: Determine client name from hostname + set_fact: + client_name: "{{ inventory_hostname }}" + + - name: Load client secrets + community.sops.load_vars: + file: "{{ playbook_dir }}/../../secrets/clients/{{ client_name }}.sops.yaml" + name: client_secrets + age_key: "{{ lookup('env', 'SOPS_AGE_KEY_FILE') }}" + no_log: true + + - name: Set Zitadel domain from secrets + set_fact: + zitadel_domain: "{{ client_secrets.zitadel_domain }}" + when: client_secrets.zitadel_domain is defined + + roles: + - role: zitadel + + post_tasks: + - name: Display deployment summary + debug: + msg: | + Deployment complete for client: {{ client_name }} + + Zitadel: https://{{ zitadel_domain }} + + Next steps: + 1. Login to Zitadel with the admin credentials + 2. Change the admin password + 3. Configure OIDC applications for Nextcloud (when deployed) diff --git a/ansible/roles/traefik/defaults/main.yml b/ansible/roles/traefik/defaults/main.yml index 41801e4..47fb0a6 100644 --- a/ansible/roles/traefik/defaults/main.yml +++ b/ansible/roles/traefik/defaults/main.yml @@ -1,8 +1,8 @@ --- # Default variables for traefik role -# Traefik version -traefik_version: "v3.0" +# Traefik version (v3.2+ fixes Docker API compatibility) +traefik_version: "v3.2" # Let's Encrypt configuration traefik_acme_email: "admin@example.com" # Override this! diff --git a/ansible/roles/traefik/templates/docker-compose.yml.j2 b/ansible/roles/traefik/templates/docker-compose.yml.j2 index 2379c4f..a549194 100644 --- a/ansible/roles/traefik/templates/docker-compose.yml.j2 +++ b/ansible/roles/traefik/templates/docker-compose.yml.j2 @@ -6,6 +6,9 @@ services: image: traefik:{{ traefik_version }} container_name: traefik restart: unless-stopped + environment: + # Fix Docker API version compatibility - use 1.44 for modern Docker + - DOCKER_API_VERSION=1.44 security_opt: - no-new-privileges:true ports: diff --git a/ansible/roles/traefik/templates/dynamic.yml.j2 b/ansible/roles/traefik/templates/dynamic.yml.j2 index 2cc996d..01bb8a1 100644 --- a/ansible/roles/traefik/templates/dynamic.yml.j2 +++ b/ansible/roles/traefik/templates/dynamic.yml.j2 @@ -2,7 +2,33 @@ # Managed by Ansible - do not edit manually http: + routers: + # Zitadel identity provider + zitadel: + rule: "Host(`zitadel.test.vrije.cloud`)" + service: zitadel + entryPoints: + - websecure + tls: + certResolver: letsencrypt + middlewares: + - zitadel-headers + + services: + # Zitadel service + zitadel: + loadBalancer: + servers: + - url: "h2c://zitadel:8080" + middlewares: + # Zitadel-specific headers + zitadel-headers: + headers: + stsSeconds: 31536000 + stsIncludeSubdomains: true + stsPreload: true + # Security headers security-headers: headers: diff --git a/ansible/roles/zitadel/defaults/main.yml b/ansible/roles/zitadel/defaults/main.yml new file mode 100644 index 0000000..00a652f --- /dev/null +++ b/ansible/roles/zitadel/defaults/main.yml @@ -0,0 +1,33 @@ +--- +# Zitadel Default Variables + +# Zitadel version (pin explicitly) +zitadel_version: "v2.63.7" + +# PostgreSQL version for Zitadel database +postgres_version: "16-alpine" + +# Admin user (password from secrets) +zitadel_admin_username: "admin" + +# Console client ID (Zitadel's built-in admin console) +zitadel_console_client_id: "251896714278772225@ptt" + +# OIDC configuration +zitadel_oidc_token_lifetime: "12h" +zitadel_oidc_refresh_lifetime: "720h" + +# Resource limits +zitadel_memory_limit: "512M" +zitadel_cpu_limit: "1.0" + +# Database configuration +zitadel_db_user: "zitadel" +zitadel_db_name: "zitadel" + +# Network configuration +zitadel_network: "zitadel-internal" +zitadel_traefik_network: "traefik" + +# Directory for Zitadel configuration +zitadel_config_dir: "/opt/docker/zitadel" diff --git a/ansible/roles/zitadel/handlers/main.yml b/ansible/roles/zitadel/handlers/main.yml new file mode 100644 index 0000000..aef7e07 --- /dev/null +++ b/ansible/roles/zitadel/handlers/main.yml @@ -0,0 +1,9 @@ +--- +# Handlers for Zitadel role + +- name: Restart Zitadel + community.docker.docker_compose_v2: + project_src: "{{ zitadel_config_dir }}" + services: + - zitadel + state: restarted diff --git a/ansible/roles/zitadel/tasks/bootstrap.yml b/ansible/roles/zitadel/tasks/bootstrap.yml new file mode 100644 index 0000000..b3d8cb0 --- /dev/null +++ b/ansible/roles/zitadel/tasks/bootstrap.yml @@ -0,0 +1,32 @@ +--- +# Bootstrap tasks for initial Zitadel configuration + +- name: Check if bootstrap already completed + stat: + path: "{{ zitadel_config_dir }}/.bootstrap_complete" + register: bootstrap_flag + +- name: Bootstrap Zitadel instance + when: not bootstrap_flag.stat.exists + block: + - name: Display admin credentials for first login + debug: + msg: | + Zitadel is now running at https://{{ zitadel_domain }} + + Login with: + Username: {{ zitadel_admin_username }} + Password: {{ client_secrets.zitadel_admin_password }} + + IMPORTANT: Change this password after first login! + + - name: Mark bootstrap as complete + file: + path: "{{ zitadel_config_dir }}/.bootstrap_complete" + state: touch + mode: '0600' + +- name: Bootstrap already completed + debug: + msg: "Zitadel bootstrap already completed, skipping initialization" + when: bootstrap_flag.stat.exists diff --git a/ansible/roles/zitadel/tasks/docker.yml b/ansible/roles/zitadel/tasks/docker.yml new file mode 100644 index 0000000..557b666 --- /dev/null +++ b/ansible/roles/zitadel/tasks/docker.yml @@ -0,0 +1,49 @@ +--- +# Docker Compose setup for Zitadel + +- name: Create Zitadel configuration directory + file: + path: "{{ zitadel_config_dir }}" + state: directory + mode: '0755' + +- name: Create Zitadel internal network + community.docker.docker_network: + name: "{{ zitadel_network }}" + driver: bridge + internal: true + +- name: Deploy Zitadel Docker Compose configuration + template: + src: docker-compose.zitadel.yml.j2 + dest: "{{ zitadel_config_dir }}/docker-compose.yml" + mode: '0600' + notify: Restart Zitadel + +- name: Start Zitadel services + community.docker.docker_compose_v2: + project_src: "{{ zitadel_config_dir }}" + state: present + register: zitadel_deploy + +- name: Wait for Zitadel database to be ready + community.docker.docker_container_exec: + container: zitadel-db + command: pg_isready -U {{ zitadel_db_user }} -d {{ zitadel_db_name }} + register: db_ready + until: db_ready.rc == 0 + retries: 30 + delay: 2 + changed_when: false + +- name: Wait for Zitadel to be healthy + uri: + url: "https://{{ zitadel_domain }}/debug/ready" + method: GET + status_code: 200 + validate_certs: yes + register: zitadel_health + until: zitadel_health.status == 200 + retries: 30 + delay: 10 + changed_when: false diff --git a/ansible/roles/zitadel/tasks/main.yml b/ansible/roles/zitadel/tasks/main.yml new file mode 100644 index 0000000..fc9dc1f --- /dev/null +++ b/ansible/roles/zitadel/tasks/main.yml @@ -0,0 +1,13 @@ +--- +# Main tasks file for Zitadel role + +- name: Include Docker Compose setup + include_tasks: docker.yml + +- name: Include bootstrap setup + include_tasks: bootstrap.yml + when: zitadel_bootstrap | default(true) + +- name: Include OIDC applications setup + include_tasks: oidc-apps.yml + when: zitadel_create_oidc_apps | default(false) diff --git a/ansible/roles/zitadel/tasks/oidc-apps.yml b/ansible/roles/zitadel/tasks/oidc-apps.yml new file mode 100644 index 0000000..552f344 --- /dev/null +++ b/ansible/roles/zitadel/tasks/oidc-apps.yml @@ -0,0 +1,7 @@ +--- +# OIDC Application creation tasks (for Nextcloud and other apps) +# This will be implemented in a later phase when Nextcloud is deployed + +- name: OIDC applications placeholder + debug: + msg: "OIDC application creation will be implemented when Nextcloud role is ready" diff --git a/ansible/roles/zitadel/templates/docker-compose.zitadel.yml.j2 b/ansible/roles/zitadel/templates/docker-compose.zitadel.yml.j2 new file mode 100644 index 0000000..688dc15 --- /dev/null +++ b/ansible/roles/zitadel/templates/docker-compose.zitadel.yml.j2 @@ -0,0 +1,99 @@ +services: + zitadel: + image: ghcr.io/zitadel/zitadel:{{ zitadel_version }} + container_name: zitadel + restart: unless-stopped + command: start-from-init --masterkeyFromEnv --tlsMode external + environment: + # Masterkey for encryption + ZITADEL_MASTERKEY: "{{ client_secrets.zitadel_masterkey }}" + + # Database configuration + ZITADEL_DATABASE_POSTGRES_HOST: zitadel-db + ZITADEL_DATABASE_POSTGRES_PORT: 5432 + ZITADEL_DATABASE_POSTGRES_DATABASE: "{{ zitadel_db_name }}" + ZITADEL_DATABASE_POSTGRES_USER_USERNAME: "{{ zitadel_db_user }}" + ZITADEL_DATABASE_POSTGRES_USER_PASSWORD: "{{ client_secrets.zitadel_db_password }}" + ZITADEL_DATABASE_POSTGRES_USER_SSL_MODE: disable + ZITADEL_DATABASE_POSTGRES_ADMIN_USERNAME: "{{ zitadel_db_user }}" + ZITADEL_DATABASE_POSTGRES_ADMIN_PASSWORD: "{{ client_secrets.zitadel_db_password }}" + ZITADEL_DATABASE_POSTGRES_ADMIN_SSL_MODE: disable + + # External domain configuration + ZITADEL_EXTERNALSECURE: "true" + ZITADEL_EXTERNALDOMAIN: "{{ zitadel_domain }}" + ZITADEL_EXTERNALPORT: 443 + + # First instance configuration + ZITADEL_FIRSTINSTANCE_ORG_NAME: "{{ client_name | title }}" + ZITADEL_FIRSTINSTANCE_ORG_HUMAN_USERNAME: "{{ zitadel_admin_username }}" + ZITADEL_FIRSTINSTANCE_ORG_HUMAN_PASSWORD: "{{ client_secrets.zitadel_admin_password }}" + ZITADEL_FIRSTINSTANCE_ORG_HUMAN_EMAIL: "admin@{{ zitadel_domain }}" + ZITADEL_FIRSTINSTANCE_ORG_HUMAN_EMAIL_VERIFIED: "true" + + networks: + - {{ zitadel_traefik_network }} + - {{ zitadel_network }} + + depends_on: + zitadel-db: + condition: service_healthy + + labels: + - "traefik.enable=true" + - "traefik.http.routers.zitadel.rule=Host(`{{ zitadel_domain }}`)" + - "traefik.http.routers.zitadel.tls=true" + - "traefik.http.routers.zitadel.tls.certresolver=letsencrypt" + - "traefik.http.routers.zitadel.entrypoints=websecure" + - "traefik.http.services.zitadel.loadbalancer.server.port=8080" + # gRPC support for API + - "traefik.http.services.zitadel.loadbalancer.server.scheme=h2c" + # Middleware for security headers + - "traefik.http.routers.zitadel.middlewares=zitadel-headers" + - "traefik.http.middlewares.zitadel-headers.headers.stsSeconds=31536000" + - "traefik.http.middlewares.zitadel-headers.headers.stsIncludeSubdomains=true" + - "traefik.http.middlewares.zitadel-headers.headers.stsPreload=true" + + deploy: + resources: + limits: + memory: {{ zitadel_memory_limit }} + cpus: "{{ zitadel_cpu_limit }}" + + zitadel-db: + image: postgres:{{ postgres_version }} + container_name: zitadel-db + restart: unless-stopped + environment: + POSTGRES_USER: "{{ zitadel_db_user }}" + POSTGRES_PASSWORD: "{{ client_secrets.zitadel_db_password }}" + POSTGRES_DB: "{{ zitadel_db_name }}" + + volumes: + - zitadel-db-data:/var/lib/postgresql/data + + networks: + - {{ zitadel_network }} + + healthcheck: + test: ["CMD-SHELL", "pg_isready -U {{ zitadel_db_user }} -d {{ zitadel_db_name }}"] + interval: 5s + timeout: 5s + retries: 5 + + deploy: + resources: + limits: + memory: 256M + cpus: "0.5" + +volumes: + zitadel-db-data: + driver: local + +networks: + {{ zitadel_traefik_network }}: + external: true + {{ zitadel_network }}: + driver: bridge + internal: true diff --git a/secrets/clients/test.sops.yaml b/secrets/clients/test.sops.yaml index 323cea9..f9c316b 100644 --- a/secrets/clients/test.sops.yaml +++ b/secrets/clients/test.sops.yaml @@ -1,32 +1,33 @@ -#ENC[AES256_GCM,data:nK9yR3JOQB56nTI8H8g2Mp7vnb8wArivoQ==,iv:Ke/n7VHkQs1X5b8/kj7Put6BNuZvK5A1WDLdVNJvPAg=,tag:kLvhJ3KCBnO6qd9KSD0OLw==,type:comment] -#ENC[AES256_GCM,data:L8VurAyFOT0RdJXab18xpAgW0ZULY9nxw/DdJ9kisEBfT+m0FZU=,iv:xv1i5wLoOR7x2N2ukuasGCrK2N5xHlfDdnwhaL+XBm8=,tag:LIzSGObX5NMloLU01T0iwg==,type:comment] -#ENC[AES256_GCM,data:vMG1ExYmlXI1RWbQWyUdmKNCqg==,iv:l9TGHsz2KOqF1i6j39ftXxUYvlfAzXY5Bi5nAJMWSQA=,tag:cJ3lrld4vL7uJOnqQRmJjQ==,type:comment] -client_name: ENC[AES256_GCM,data:MZWftQ==,iv:f5MS6vLBC+tHJlB+VWTpOWTej7+sJZKbioMfA37ZjiA=,tag:/h+aYKOh4BPh96CJlNzpJw==,type:str] -client_domain: ENC[AES256_GCM,data:CtSIb4/bQU8etWJpTqudxZwhuUk+hqU=,iv:FZhFwV88FglVQzjgPNJW5ZizJtHQbfdFaUbeLWaU1io=,tag:3VNoF543JENvPbiLU/todg==,type:str] -#ENC[AES256_GCM,data:BdAVkrQXKwMUuNr+P8iqGA==,iv:8Acn7K+tR2b8mkPe5EugAKpV9A540FVJC0kIuDQIPD4=,tag:jdfmiZ5o4dy8Q/YbZqq/ng==,type:comment] -zitadel_db_password: ENC[AES256_GCM,data:Jt6C7U88Ale90QxSm7E4ZwluIbuLq3tWl0+tOFDpzP2og7eDyQ==,iv:fqvUJcK8h0xRSAzxsVOwSUyyL2CKlyvszCihL4syot8=,tag:rDXQIQ/0MDRftkDuz50Tkw==,type:str] -zitadel_admin_password: ENC[AES256_GCM,data:apcy1CuWpICWULo8VULqH47loeFB3eUKLvUBIuVXIuu7BPwtbw==,iv:U7JB0wDhGKPwpRs1RE0X8dfcuE7sa5b9ikc+0XDWKos=,tag:YcT3y+1wRLfHsYoM4c/2yg==,type:str] -zitadel_masterkey: ENC[AES256_GCM,data:PZHiQK3Z2IGE0DUp/DRsQ7omfNM0xKmiaPAQHn1D4vU1XuJ2t54=,iv:K11E24TK6886crExpEWF/eDF53w5lQzIt5BG5jS557Q=,tag:NFNY7rTdxKLUOxglCfdYRw==,type:str] -#ENC[AES256_GCM,data:UCxVKl+EGvZvHFHZa91rjlYI,iv:RmN4jI05bkM1uEE1TglzE5a54RYFYMzCMQvlpq2ydbg=,tag:POsM86j4Jj+8wjwF7ffWgA==,type:comment] -nextcloud_db_password: ENC[AES256_GCM,data:oN+PC7pD07VyV9bKqZOGWLkdH6VhKOz+BBRmPYmm/8q4OQ47iQ==,iv:8ZAipySlXTgZm50R+AOKWQGszc+fcgKPMoa+TOFq+ig=,tag:6pEqOidt8Dz4P0QQ+7u+BQ==,type:str] -nextcloud_admin_password: ENC[AES256_GCM,data:ZMoK4M6xAFK3DQIBMn0a1mtkKCjhW6P/dLfUUILccnjmqO8a3A==,iv:ctXQhhO5NK5/i2Hg73lnCy1bHlgXsgBjMxQPhJy2yrw=,tag:LzbWUwZ2AEQyx2lbshu73Q==,type:str] -#ENC[AES256_GCM,data:J9fmtOMRn7VCA4qn3KN5L4QXuaNLBmk35q8NlqxMYg5TJg==,iv:f7DM3G1VN3rvIkFzAJrouG4d1A2jRtNWuJu5/+YezMQ=,tag:WA4qDSQH5+NSs/8tiHNM6Q==,type:comment] -restic_repo_password: ENC[AES256_GCM,data:V3Pw2hZIFWD/uK+pXPETHNAula4SfmPQGEOEqw/v7KdcwMlhgQ==,iv:DukqTm/LtliLioALDwZI0BDW3sJwNfq/6vcHVIit0Gc=,tag:mNIHwcBtranAmJNBTl4thw==,type:str] -#ENC[AES256_GCM,data:Jnxs5WoVDE31NgQmocYH80W569qK8yHDwY8ZDYeDyOY+Fn1mbK7xdilCB4aOn4vP5qtMzqCKa7paXm78BzZ9FpRgAkY=,iv:kgLwRvT5XhgDN7O4yEYkxMVFCuNtAdB+mmhYjar1pqY=,tag:fplXZLOILDOzh7n8WIWm/A==,type:comment] -#ENC[AES256_GCM,data:xpilPXQdvCRTIBjWEfRZMfILlWi/gDGL7onkT9o=,iv:1XsMusNaqv80/TLLfdrBk56RqNCDTB0EavhQXBJVS3I=,tag:OXlrYZuldsJeyaJQBWeDPQ==,type:comment] -#ENC[AES256_GCM,data:hwTLhDd5S4EWFFBcrkxGRazBVU50txHIKjKyOb3VJOqF,iv:7DGf0PvBKYN9NxhAiAi2bGThWf7jHmAhJDuqgGb+7+4=,tag:SO35c4iT8hBDlorx/6I8ww==,type:comment] +#ENC[AES256_GCM,data:N5GrnX4oxwTmii6SiAdbZ6cNHYHS8COphg==,iv:nKwJFhRd+lKsKvTY/miXkvNYF/MoPOuTCcMOldB1e6o=,tag:Gu7VGqLmHnNFSBq23oiTKA==,type:comment] +#ENC[AES256_GCM,data:OZwfwJ9O+xSygmBOirZ3OfKzRQ==,iv:2Oy+ZZnfVgKB9rm1Xr+4dVY0Ny3soiVdznYHT+KV2Mk=,tag:wBr1c3h+118FJE5zRB9D7g==,type:comment] +client_name: ENC[AES256_GCM,data:vaMWvg==,iv:SNkcJsVq0QHnCku9WzOHZpY282OYK3NpWdnWpr9f0Cc=,tag:CMA6AY0I4F+EQ5Vw14Fo9Q==,type:str] +client_domain: ENC[AES256_GCM,data:4QqtTVrKzr9RihL2MjCfdQ==,iv:6EiqWtRBuFfBO28NJkHfGaPUMAPkd6XpU9jKJyJN3AQ=,tag:sbm9ArjDeUjQnRUcberm3g==,type:str] +#ENC[AES256_GCM,data:uoS37xiQ+sj9tL7TAPnY,iv:D+YkgPUiYQKgGMircnGZjhZ+9qqwDGiVDDfHpw76Irs=,tag:IF/n/bZJkXnnQLEZpMCjog==,type:comment] +zitadel_domain: ENC[AES256_GCM,data:zBYb/6VV9w+WaRJ979rOAexrjKDWrKA3,iv:xTEbPUNSANoIMlIYnejj+DpSyg3G1zp9dLExUap0FiI=,tag:zc6PsNTAyVug1j7qV2QBLg==,type:str] +#ENC[AES256_GCM,data:Et/LOKSvoypnWgOa+2BRDw==,iv:GnOn/0zgCWJyaxQU8EuVWf6JMvUishkOLgiX3u4firg=,tag:aSiwqpjeCaYkHfvK9yA2EA==,type:comment] +zitadel_db_password: ENC[AES256_GCM,data:DYUfwlU+MmgMVhPNG2vWelP8AxoZGBBdST6Tu3qL9oo=,iv:6rUUndg7lKVUTBleDN296csG18Sge+jfcGAS8nLnvNQ=,tag:eZYsJajreqBBDQ9f62uEkg==,type:str] +zitadel_admin_password: ENC[AES256_GCM,data:R63L/AasVX4U9JTk6TceQ3ssQmauqA==,iv:vdFpadKrbnYabbF1VHz9p1F1UAGTq8zGimfUcY1Q18I=,tag:lJzlPLjWim7un8bnu6Ag0w==,type:str] +zitadel_masterkey: ENC[AES256_GCM,data:UJJvevSA3wOdiSsNhgd6FQyanGz0UlNY07PFw/A2/oM=,iv:YllkHETB84ymAdKlVwHRtFJELOU8J16Zk+YOJERA5o8=,tag:TEg1grtk1tLekZFMYXvoCQ==,type:str] +#ENC[AES256_GCM,data:ezHDbKI3OWGK1g+Foy55zIO3,iv:qu+124Qr8HnSNITJ/KLQPfiKk+tsylPc/6pXfJus7Rw=,tag:jdK4CG1ID9vEf3fwGX21qA==,type:comment] +nextcloud_db_password: ENC[AES256_GCM,data:XcMlkgQEFPDjupgkLN29Kv9/h9zHqgbVFBESpNsNQcc=,iv:qGXo/un4a7Zvbwkfe9SalqRhYHA8aK5R36j75uwI4As=,tag:49bXKFX4LJSG6qWGnAPmOA==,type:str] +nextcloud_admin_password: ENC[AES256_GCM,data:aPrihv33Jenj14X16xCM+ad5,iv:k/azAY3tYkgD3mTK66rl8xXCk6Q5WFPyAx3x42gsL8U=,tag:DP9aLEI5aLj9nxqU2fOe/w==,type:str] +#ENC[AES256_GCM,data:+8XTcTojV2GEgJ0Vqgwi/M/dGTiQ/GjFaNtYHhfzg79pCg==,iv:kaf0tID2dgEZ34K+SStYwqXE457uYx1jJ/X/jj66QKk=,tag:Ffh4oJ/vlBDAt2ietzaIQg==,type:comment] +restic_repo_password: ENC[AES256_GCM,data:XnJ6T1yFU+bMEqeZ2DlfwSrH8fDfvSECvbpPiajqWKY=,iv:dQFidsORFC8b6xkm/SxRoW86kXZLgVdw4R5eWK3Slek=,tag:LhHnkQu6fK6q+LGE5+jQAw==,type:str] +#ENC[AES256_GCM,data:WJL9I1Ywhyj1zFLFpkzAeKJnTLcKqpXHvK6U8eReraXjll8cfDvBoy0gDjoaYJeiOUsNrUosgCowUkO5Rsh7qAkdxEM=,iv:feQbZyyVTtIEgq7r235977Qzv4Aw3ySnw1krZ4e+xbw=,tag:p5FKhxwh14rCZlr4UNtfNw==,type:comment] +#ENC[AES256_GCM,data:H7uVvy0johFigCM6gXFJefRnaN8+aHIeP24aM3A=,iv:BYipDmfPR54zldX4FYz1Zd8CldPaaFMaJexgcSlLjSY=,tag:jbbitJgtVYvoxpqoHA6Rpw==,type:comment] +#ENC[AES256_GCM,data:NSV3p9oBOEqAuunSfCOwj2IyL4NLKQ9jbWm2FyeJoQ2T,iv:ttn70OrdSNi76792DQR7cSRao8rhtygoYOoKNS86ASY=,tag:d6/OBfkwk0l/654F/8jeiA==,type:comment] sops: age: - recipient: age170jqy5pg6z62kevadqyxxekw8ryf3e394zaquw0nhs9ae3v9wd6qq2hxnk enc: | -----BEGIN AGE ENCRYPTED FILE----- - YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBWZko3UVJkSzRjK3J0bXBm - Yk0rUTRRbmxYL001QXNEMHpIaGsvMUc2ZjFnCnZnTU8ySHc0QVB6amgrTjBPdG5w - TFhucW9VZHNDWmdaVDdZWDRQbjhOQzAKLS0tIHJWWm00VWVIZlNXd04veGRoTkIw - R0kyRC9VcTFoWkFCUnl6ZmlyRjh3bXcKCkAed8Gx9jxFmoFg7vyM4a3xO9N+FxtI - CdpnZ9Wk1O498wPIV2meM3RFBclkWFgqGvAqzUNbzGuMnoSlRfJq+w== + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBTUy9RZWdWV0lQbU5kQ1Vq + SEpNVitYcmJ4YjZ2bmtMdE1WMVYyMXBrMXd3CkI3ZTBldDZrUjh0R2ROOG1LOUpR + ZmVOaHNmeXdoQzJqRFk3UUJESDE3Uk0KLS0tIEo2YjRRWm9yamRoN1pRYWgwZTBx + bUJ6cTFkWmlNRWxFS2FhRzNYbUFpb3cK27FBZIOevWweM5OUIAvM7A2ZJdI36aao + 1t8Ot5vfCh7p01Es+Sb1YlNbyTmZ1P3ZV9FNxVotEjxYRH6BZuovjg== -----END AGE ENCRYPTED FILE----- - lastmodified: "2025-12-27T13:19:53Z" - mac: ENC[AES256_GCM,data:XkD2lptFXJrRmKg/Rxd25D1Y2bGxKM+GqBcgXQTDXvr+BIrE5jC9AWzycmkB+GX0Ta5LYlcLk1mrXGp/SbNxE8rubCvqS7qZbpxEBQi8fsy+LX0kiCOgM5SSxMM6ON/gSJ2eivLzpEbeBGwXau77fNm/2MAAWZIdlfzeIN/9o4I=,iv:MU1pp6+rr7Gvs6mCPMUqz6VnPGLttUB0dgZsb43WyH4=,tag:ts54uUviBdmHif9KiQfrKQ==,type:str] + lastmodified: "2026-01-05T14:44:48Z" + mac: ENC[AES256_GCM,data:iNWtt7I33yQXTwrPf3GMJ4qC9HHmlRAVQrZyN7KFyOxT7L8iijCwbMThA8k+EVHIyQU10rYo5nbZtTkM4rJ7RiXqfwQVRpKMyLC+67hAiQBUwDhy7iVX4G1LzkJObTQnxAsldJ8O7gFReOFyTklf9WyUC2lRdcW4KkMnnDQdkao=,iv:QHJ4n75iGQ4mI4UoTUEPa/oXa4iLe7DtCzl14h5ENtU=,tag:14vV+JSLlXtxaEfwV3+Qzw==,type:str] unencrypted_suffix: _unencrypted version: 3.11.0 diff --git a/tofu/dns.tf b/tofu/dns.tf index 5afccbc..bace170 100644 --- a/tofu/dns.tf +++ b/tofu/dns.tf @@ -1,44 +1,55 @@ -# DNS Configuration -# OPTIONAL: Only used if you have a domain registered in Hetzner DNS -# Comment out this entire file if you don't have a domain yet +# DNS Configuration for vrije.cloud using hcloud provider +# The zone already exists in Hetzner Console, so we reference it as a data source -# Uncomment below when you have a domain registered in Hetzner DNS -/* -# DNS Zone (must already exist in Hetzner DNS) -data "hetznerdns_zone" "main" { +# Reference the existing DNS zone +data "hcloud_zone" "main" { name = var.base_domain } -# A Records for client servers -resource "hetznerdns_record" "client_a" { +# A Records for client servers (e.g., test.vrije.cloud -> 78.47.191.38) +resource "hcloud_zone_rrset" "client_a" { for_each = var.clients - zone_id = data.hetznerdns_zone.main.id - name = each.value.subdomain - type = "A" - value = hcloud_server.client[each.key].ipv4_address - ttl = 300 + zone = data.hcloud_zone.main.name + name = each.value.subdomain + type = "A" + ttl = 300 + records = [ + { + value = hcloud_server.client[each.key].ipv4_address + comment = "Client ${each.key} server" + } + ] } -# Wildcard A record for each client (for subdomains like auth.alpha.platform.nl) -resource "hetznerdns_record" "client_wildcard" { +# Wildcard A record for each client (e.g., *.test.vrije.cloud for zitadel.test.vrije.cloud) +resource "hcloud_zone_rrset" "client_wildcard" { for_each = var.clients - zone_id = data.hetznerdns_zone.main.id - name = "*.${each.value.subdomain}" - type = "A" - value = hcloud_server.client[each.key].ipv4_address - ttl = 300 + zone = data.hcloud_zone.main.name + name = "*.${each.value.subdomain}" + type = "A" + ttl = 300 + records = [ + { + value = hcloud_server.client[each.key].ipv4_address + comment = "Wildcard for ${each.key} subdomains (Zitadel, Nextcloud, etc)" + } + ] } -# AAAA Records for IPv6 -resource "hetznerdns_record" "client_aaaa" { +# AAAA Records for IPv6 (e.g., test.vrije.cloud IPv6) +resource "hcloud_zone_rrset" "client_aaaa" { for_each = var.clients - zone_id = data.hetznerdns_zone.main.id - name = each.value.subdomain - type = "AAAA" - value = hcloud_server.client[each.key].ipv6_address - ttl = 300 + zone = data.hcloud_zone.main.name + name = each.value.subdomain + type = "AAAA" + ttl = 300 + records = [ + { + value = hcloud_server.client[each.key].ipv6_address + comment = "Client ${each.key} server IPv6" + } + ] } -*/ diff --git a/tofu/main.tf b/tofu/main.tf index 293679a..3497bfd 100644 --- a/tofu/main.tf +++ b/tofu/main.tf @@ -3,10 +3,7 @@ provider "hcloud" { token = var.hcloud_token } -# DNS provider - uncomment when using Hetzner DNS -# provider "hetznerdns" { -# apitoken = var.hetznerdns_token -# } +# hcloud provider handles both Cloud and DNS resources # SSH Key Resource resource "hcloud_ssh_key" "default" { diff --git a/tofu/versions.tf b/tofu/versions.tf index 2bef68d..9fe67c2 100644 --- a/tofu/versions.tf +++ b/tofu/versions.tf @@ -4,13 +4,7 @@ terraform { required_providers { hcloud = { source = "hetznercloud/hcloud" - version = "~> 1.45" + version = "~> 1.57" } - # DNS provider - optional, only needed if using Hetzner DNS - # Commented out since DNS is not required initially - # hetznerdns = { - # source = "timohirt/hetznerdns" - # version = "~> 2.4" - # } } }