From 1c36b7bbcb25a85db102e2863e032a6359f35750 Mon Sep 17 00:00:00 2001 From: ml Date: Sun, 28 Jun 2026 15:39:42 +0200 Subject: [PATCH] Add Vaultwarden self-hosted password vault at vault.ladkau.de MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - New vaultwarden role — Vaultwarden container with SQLite storage, admin panel protected by token, Keycloak SSO enabled on second deploy after the OIDC client secret is available (SSO_ENABLED conditionally set so first provisioning deploy works without Keycloak being configured yet) - Traefik routes vault.ladkau.de with lax rate limiting (SPA loads many assets) - vault.ladkau.de added to DNS table, check-services.sh, status dashboard, and check-vault.sh (admin token required; SSO secret is post-provisioning) - Configuration runbook: step 2.6 for Keycloak client, section 6 for Vaultwarden setup including admin panel, SSO login, and client configuration --- ansible/group_vars/all/vars.yml | 8 ++++ ansible/roles/dashboard/templates/app.py.j2 | 3 +- ansible/roles/vaultwarden/handlers/main.yml | 7 ++++ ansible/roles/vaultwarden/tasks/main.yml | 29 ++++++++++++++ .../templates/docker-compose.yml.j2 | 37 +++++++++++++++++ ansible/site.yml | 1 + docs/runbook-configuration.md | 40 ++++++++++++++++++- docs/runbook-provisioning.md | 16 ++++++-- scripts/check-services.sh | 1 + scripts/check-vault.sh | 1 + 10 files changed, 137 insertions(+), 6 deletions(-) create mode 100644 ansible/roles/vaultwarden/handlers/main.yml create mode 100644 ansible/roles/vaultwarden/tasks/main.yml create mode 100644 ansible/roles/vaultwarden/templates/docker-compose.yml.j2 diff --git a/ansible/group_vars/all/vars.yml b/ansible/group_vars/all/vars.yml index 583557d..e523fef 100644 --- a/ansible/group_vars/all/vars.yml +++ b/ansible/group_vars/all/vars.yml @@ -8,6 +8,7 @@ domain_sso: "sso.{{ domain_base }}" domain_mail: "mail.{{ domain_base }}" domain_registry: "cr.{{ domain_base }}" domain_k8s: "k8s.{{ domain_base }}" +domain_vault: "vault.{{ domain_base }}" # Let's Encrypt acme_email: matthias.ladkau@gmail.com @@ -97,5 +98,12 @@ registry_data_dir: /opt/registry # k8s (placeholder) k8s_data_dir: /opt/k8s +# Vaultwarden (self-hosted Bitwarden-compatible password vault) +vaultwarden_version: "latest" +vaultwarden_data_dir: /opt/vaultwarden +# Secrets — store values in ansible/group_vars/all/vault.yml (Ansible Vault) +# vaultwarden_admin_token: "" # generate: openssl rand -hex 32 +# vaultwarden_sso_client_secret: "" # added after Keycloak is configured — see runbook-configuration.md step 2.6 + # Status dashboard (public — cloud.ladkau.de root) dashboard_data_dir: /opt/dashboard diff --git a/ansible/roles/dashboard/templates/app.py.j2 b/ansible/roles/dashboard/templates/app.py.j2 index 3f7ee4d..7a6c9b5 100644 --- a/ansible/roles/dashboard/templates/app.py.j2 +++ b/ansible/roles/dashboard/templates/app.py.j2 @@ -22,7 +22,8 @@ SERVICES = [ ("Keycloak", "https://{{ domain_sso }}/realms/master", 200, "https://{{ domain_sso }}"), ("Roundcube", "https://{{ domain_mail }}", 200, "https://{{ domain_mail }}"), ("Registry", "https://{{ domain_registry }}/v2/", 401, "https://{{ domain_registry }}"), - ("k8s", "https://{{ domain_k8s }}", 200, "https://{{ domain_k8s }}"), + ("k8s", "https://{{ domain_k8s }}", 200, "https://{{ domain_k8s }}"), + ("Vaultwarden", "https://{{ domain_vault }}", 200, "https://{{ domain_vault }}"), ] {% raw %} diff --git a/ansible/roles/vaultwarden/handlers/main.yml b/ansible/roles/vaultwarden/handlers/main.yml new file mode 100644 index 0000000..6fa6070 --- /dev/null +++ b/ansible/roles/vaultwarden/handlers/main.yml @@ -0,0 +1,7 @@ +--- +- name: Restart vaultwarden + community.docker.docker_compose_v2: + project_src: "{{ vaultwarden_data_dir }}" + state: present + pull: missing + recreate: always diff --git a/ansible/roles/vaultwarden/tasks/main.yml b/ansible/roles/vaultwarden/tasks/main.yml new file mode 100644 index 0000000..3270770 --- /dev/null +++ b/ansible/roles/vaultwarden/tasks/main.yml @@ -0,0 +1,29 @@ +--- +- name: Create Vaultwarden data directories + ansible.builtin.file: + path: "{{ item }}" + state: directory + owner: root + group: root + mode: "0755" + loop: + - "{{ vaultwarden_data_dir }}" + - "{{ vaultwarden_data_dir }}/data" + tags: vaultwarden + +- name: Deploy Docker Compose file + ansible.builtin.template: + src: docker-compose.yml.j2 + dest: "{{ vaultwarden_data_dir }}/docker-compose.yml" + owner: root + group: root + mode: "0644" + notify: Restart vaultwarden + tags: vaultwarden + +- name: Start Vaultwarden + community.docker.docker_compose_v2: + project_src: "{{ vaultwarden_data_dir }}" + state: present + pull: missing + tags: vaultwarden diff --git a/ansible/roles/vaultwarden/templates/docker-compose.yml.j2 b/ansible/roles/vaultwarden/templates/docker-compose.yml.j2 new file mode 100644 index 0000000..ce04b14 --- /dev/null +++ b/ansible/roles/vaultwarden/templates/docker-compose.yml.j2 @@ -0,0 +1,37 @@ +# Managed by Ansible — do not edit manually +services: + vaultwarden: + image: vaultwarden/server:{{ vaultwarden_version }} + container_name: vaultwarden + restart: unless-stopped + environment: + DOMAIN: "https://{{ domain_vault }}" + # Admin panel at /admin/ — use a long random token + ADMIN_TOKEN: "{{ vaultwarden_admin_token }}" + # Disable public sign-up — accounts created via SSO or admin panel + SIGNUPS_ALLOWED: "false" +{% if vaultwarden_sso_client_secret | default('') %} + # SSO via Keycloak — enabled after Keycloak client is configured (step 2.6) + SSO_ENABLED: "true" + SSO_CLIENT_ID: "vaultwarden" + SSO_CLIENT_SECRET: "{{ vaultwarden_sso_client_secret }}" + SSO_AUTHORITY: "https://{{ domain_sso }}/realms/ladkau" +{% else %} + SSO_ENABLED: "false" +{% endif %} + volumes: + - {{ vaultwarden_data_dir }}/data:/data + networks: + - traefik_public + labels: + - "traefik.enable=true" + - "traefik.http.routers.vaultwarden.rule=Host(`{{ domain_vault }}`)" + - "traefik.http.routers.vaultwarden.entrypoints=websecure" + - "traefik.http.routers.vaultwarden.tls.certresolver=letsencrypt" + - "traefik.http.services.vaultwarden.loadbalancer.server.port=80" + # Lax rate limit — Vaultwarden SPA loads many assets on first visit + - "traefik.http.routers.vaultwarden.middlewares=rate-limit-lax@docker" + +networks: + traefik_public: + external: true diff --git a/ansible/site.yml b/ansible/site.yml index 4b329b7..7ab29d5 100644 --- a/ansible/site.yml +++ b/ansible/site.yml @@ -12,4 +12,5 @@ - mail - registry - k8s + - vaultwarden - dashboard diff --git a/docs/runbook-configuration.md b/docs/runbook-configuration.md index 9d49e16..05d854f 100644 --- a/docs/runbook-configuration.md +++ b/docs/runbook-configuration.md @@ -23,6 +23,7 @@ The script checks these endpoints and verifies the expected HTTP status code: | `https://mail.ladkau.de` | 200 | Roundcube webmail login | | `https://cr.ladkau.de/v2/` | 401 | Registry API — auth required, correct without credentials | | `https://k8s.ladkau.de` | 200 | Placeholder page | +| `https://vault.ladkau.de` | 200 | Vaultwarden web vault | A `000` result means the connection was refused or timed out — a container that did not start. Keycloak and Nextcloud may return `502` for up to 90 seconds on @@ -91,6 +92,21 @@ For each user you want to be able to log in to Gitea and Nextcloud: 7. Go to the **Credentials** tab and copy the **Client secret** — you will need it in step 4. +### 2.6 Create the Vaultwarden OIDC client + +1. **Clients** → **Create client**. +2. **Client type**: OpenID Connect — **Client ID**: `vaultwarden` → **Next**. +3. Turn **Client authentication** on → **Next**. +4. **Valid redirect URIs**: `https://vault.ladkau.de/*` +5. **Valid post logout redirect URIs**: `https://vault.ladkau.de/*` +6. **Web origins**: `https://vault.ladkau.de` → **Save**. +7. Go to the **Credentials** tab and copy the **Client secret**. +8. Add it to the vault as `vaultwarden_sso_client_secret` and redeploy: + ```bash + ansible-vault edit ansible/group_vars/all/vault.yml + ansible-playbook -i ansible/inventory.ini ansible/site.yml --tags vaultwarden --ask-vault-pass + ``` + ## 3. Gitea ### 3.1 Create the admin account @@ -196,7 +212,29 @@ imapsync \ | Security | SSL/TLS | | Username | as set in vault | -## 6. Container registry +## 6. Vaultwarden + +Vaultwarden is a self-hosted Bitwarden-compatible password vault. Any official +Bitwarden client (browser extension, desktop, mobile) can connect to it. + +### 6.1 Log in to the admin panel + +Go to `https://vault.ladkau.de/admin/` and enter the `vaultwarden_admin_token` +from the vault. From here you can invite users and manage the instance. + +### 6.2 Sign in with Keycloak SSO + +Users can sign in at `https://vault.ladkau.de` using the **Enterprise Single +Sign-On** button — enter `https://sso.ladkau.de/realms/ladkau` as the +organisation identifier. On first login Vaultwarden creates an account +automatically (public self-registration is otherwise disabled). + +### 6.3 Connect a Bitwarden client + +In any official Bitwarden client, set the **Server URL** to +`https://vault.ladkau.de` before logging in. + +## 7. Container registry ```bash # Login diff --git a/docs/runbook-provisioning.md b/docs/runbook-provisioning.md index 295b0c4..a5ca30d 100644 --- a/docs/runbook-provisioning.md +++ b/docs/runbook-provisioning.md @@ -78,6 +78,7 @@ Let's Encrypt certificates on first start and DNS must resolve at that point. | mail.ladkau.de | A → server IP | | cr.ladkau.de | A → server IP | | k8s.ladkau.de | A → server IP | +| vault.ladkau.de | A → server IP | ### 4. Create the vault and populate secrets @@ -118,17 +119,23 @@ registry_users: - username: alice password: "your-password" -# Dovecot IMAP users — generate with: openssl rand -hex 32 +# Vaultwarden password vault +vaultwarden_admin_token: "" # generate: openssl rand -hex 32 +# vaultwarden_sso_client_secret is added after Keycloak is configured — see +# step 2.6 of runbook-configuration.md + +# Dovecot IMAP users dovecot_users: - username: alice password: "your-password" # Fetchmail — external POP3 accounts to pull from (omit section if not needed) +# local_user must match a username defined in dovecot_users above # fetchmail_accounts: # - server: pop.gmail.com # username: user@gmail.com # password: app-password # use a Gmail App Password, not your main password -# local_user: alice +# local_user: alice # must match a dovecot_users entry # ssl: true # keep: true # set false to delete from source after fetching # poll_minutes: 10 # how often to poll this account (default: 10) @@ -188,8 +195,9 @@ This runs all roles in order: | 6 | `sso` | Keycloak single sign-on with PostgreSQL | | 7 | `mail` | Roundcube webmail client with PostgreSQL | | 8 | `registry` | Docker Registry v2 with htpasswd auth | -| 9 | `k8s` | Placeholder page at k8s.ladkau.de | -| 10 | `dashboard` | Public status dashboard at cloud.ladkau.de — service health and server stats | +| 9 | `k8s` | Placeholder page at k8s.ladkau.de | +| 10 | `vaultwarden` | Vaultwarden password vault at vault.ladkau.de | +| 11 | `dashboard` | Public status dashboard at cloud.ladkau.de — service health and server stats | To apply a single role: diff --git a/scripts/check-services.sh b/scripts/check-services.sh index 54bf5ea..c02720d 100644 --- a/scripts/check-services.sh +++ b/scripts/check-services.sh @@ -16,6 +16,7 @@ CHECKS=( "https://mail.ladkau.de 200 10" "https://cr.ladkau.de/v2/ 401 10" "https://k8s.ladkau.de 200 10" + "https://vault.ladkau.de 200 10" ) if ! command -v curl &>/dev/null; then diff --git a/scripts/check-vault.sh b/scripts/check-vault.sh index 7c858b1..8e8c726 100755 --- a/scripts/check-vault.sh +++ b/scripts/check-vault.sh @@ -39,6 +39,7 @@ required_scalars = [ "nextcloud_admin_password", "roundcube_db_password", "roundcube_des_key", + "vaultwarden_admin_token", ] # Required non-empty lists (must contain at least one entry)