Deploy Zitadel identity provider with DNS automation (#3) (#8)

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 <pieter@kolabnow.com>
Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Pieter van Boheemen 2026-01-05 16:40:37 +01:00 committed by GitHub
parent 6bc8e508c6
commit 054e0e1e87
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 383 additions and 66 deletions

View file

@ -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)

View file

@ -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!

View file

@ -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:

View file

@ -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:

View file

@ -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"

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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"

View file

@ -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

View file

@ -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

View file

@ -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"
}
]
}
*/

View file

@ -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" {

View file

@ -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"
# }
}
}