Fix provisioning errors found during first real run

- ansible.cfg: suppress Python interpreter discovery warning
- ansible/inventory.ini: add ansible_host; server IP is now defined in
  one place and read dynamically by run-bootstrap.sh
- group_vars/all/ → directory layout so vault.yml is auto-loaded by Ansible
  (previously vault.yml did not match any group name and was silently ignored)
- scripts/run-bootstrap.sh: read server IP from inventory, chmod 600 private
  keys automatically, show actual SSH error when root login fails
- scripts/bootstrap-deploy-user.sh: add sudoers.d entry for passwordless sudo
  (deploy user has no password so sudo group membership alone was not enough)
- nextcloud: fix Redis healthcheck (CMD-SHELL pipe was unreliable in Alpine,
  switched to CMD form with start_period)
- group_vars/all/vars.yml: fix Roundcube image tag (1.6-apache and 1.6 do not
  exist; correct tag is 1.6.x-apache)
- docs/runbook.md: expand prerequisites (SSH keys section), add step 1 for
  setting the server IP, expand bootstrap step with preflight detail, fix
  step numbering
This commit is contained in:
ml
2026-06-28 05:53:29 +02:00
parent 03d2f6d57a
commit a462ff1729
9 changed files with 87 additions and 37 deletions
+1 -1
View File
@@ -2,5 +2,5 @@ keys/*
!keys/*.*
# Ansible Vault secrets — never commit plain-text secrets
ansible/group_vars/vault.yml
ansible/group_vars/all/vault.yml
+2
View File
@@ -0,0 +1,2 @@
[defaults]
interpreter_python = auto_silent
@@ -26,7 +26,7 @@ traefik_version: v3.3
traefik_data_dir: /opt/traefik
# htpasswd-formatted user list for the dashboard.
# Generate with: echo $(htpasswd -nB admin) | sed -e 's/\$/\$\$/g'
# Store the actual value in ansible/group_vars/vault.yml (Ansible Vault).
# Store the actual value in ansible/group_vars/all/vault.yml (Ansible Vault).
traefik_dashboard_users: ""
# Gitea
@@ -36,7 +36,7 @@ gitea_data_dir: /opt/gitea
# Set to false for the very first deploy so the admin account can be created,
# then flip to true and redeploy to close public registration.
gitea_disable_registration: true
# Secrets — store values in ansible/group_vars/vault.yml (Ansible Vault)
# Secrets — store values in ansible/group_vars/all/vault.yml (Ansible Vault)
# gitea_db_password: ""
# gitea_secret_key: "" # generate: openssl rand -hex 32
# gitea_internal_token: "" # generate: openssl rand -hex 32
@@ -46,7 +46,7 @@ keycloak_version: "26.2"
keycloak_db_version: "16"
sso_data_dir: /opt/sso
keycloak_admin_user: admin
# Secrets — store values in ansible/group_vars/vault.yml (Ansible Vault)
# Secrets — store values in ansible/group_vars/all/vault.yml (Ansible Vault)
# keycloak_db_password: ""
# keycloak_admin_password: ""
@@ -55,12 +55,12 @@ nextcloud_version: "31-apache" # apache variant includes a working web server
nextcloud_db_version: "16"
nextcloud_data_dir: /opt/nextcloud
nextcloud_admin_user: admin
# Secrets — store values in ansible/group_vars/vault.yml (Ansible Vault)
# Secrets — store values in ansible/group_vars/all/vault.yml (Ansible Vault)
# nextcloud_db_password: ""
# nextcloud_admin_password: ""
# Mail (Roundcube webmail client)
roundcube_version: "1.6-apache"
roundcube_version: "1.6.x-apache"
roundcube_db_version: "16"
mail_data_dir: /opt/mail
# IMAP/SMTP — set to the mail server Roundcube should connect to.
@@ -70,7 +70,7 @@ roundcube_imap_host: ""
roundcube_imap_port: "993"
roundcube_smtp_host: ""
roundcube_smtp_port: "587"
# Secrets — store values in ansible/group_vars/vault.yml (Ansible Vault)
# Secrets — store values in ansible/group_vars/all/vault.yml (Ansible Vault)
# roundcube_db_password: ""
# roundcube_des_key: "" # generate: openssl rand -hex 12 (must be exactly 24 chars)
@@ -79,7 +79,7 @@ registry_data_dir: /opt/registry
# registry_htpasswd — full htpasswd file content, store in vault.yml
# Generate with: docker run --entrypoint htpasswd httpd:2 -Bbn <user> <password>
# Multiple users: run the command once per user and concatenate the lines.
# Secrets — store values in ansible/group_vars/vault.yml (Ansible Vault)
# Secrets — store values in ansible/group_vars/all/vault.yml (Ansible Vault)
# registry_htpasswd: ""
# k8s (placeholder)
+1 -1
View File
@@ -1,2 +1,2 @@
[cloud]
cloud.ladkau.de ansible_user=deploy ansible_ssh_private_key_file=../keys/notroot_cloud_ladkau_de
cloud.ladkau.de ansible_host=217.154.207.148 ansible_user=deploy ansible_ssh_private_key_file=keys/notroot_cloud_ladkau_de
@@ -28,10 +28,11 @@ services:
networks:
- nextcloud_internal
healthcheck:
test: ["CMD-SHELL", "redis-cli ping | grep PONG"]
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5
start_period: 10s
nextcloud:
image: nextcloud:{{ nextcloud_version }}
+51 -24
View File
@@ -32,12 +32,24 @@ ssh-keygen -t ed25519 -f keys/notroot_cloud_ladkau_de -C "deploy@cloud.ladkau.de
```
Upload the root public key to the server via the Strato control panel (or paste
it during the initial OS install) so that `root@217.154.207.148` is accessible
before running anything.
it during the initial OS install) so that root SSH access is available before
running anything. The server IP is defined in `ansible/inventory.ini`
(`ansible_host`).
## Steps
### 1. Install Ansible collections
### 1. Set the server IP
Open `ansible/inventory.ini` and set `ansible_host` to the server's public IP:
```ini
cloud.ladkau.de ansible_host=<server-ip> ansible_user=deploy ansible_ssh_private_key_file=keys/notroot_cloud_ladkau_de
```
This is the only place the IP needs to be set — the bootstrap script and all
Ansible roles read it from here.
### 2. Install Ansible collections
From the repo root:
@@ -50,28 +62,28 @@ Required collections:
- `ansible.posix` — authorized_key module
- `community.docker` — docker_network, docker_compose_v2 modules
### 2. Configure DNS
### 3. Configure DNS
Ensure the following DNS A records point to `217.154.207.148` before running
the playbook. Traefik requests Let's Encrypt certificates on first start and
DNS must resolve at that point.
Ensure the following DNS A records all point to the server IP (`ansible_host`
in `ansible/inventory.ini`) before running the playbook. Traefik requests
Let's Encrypt certificates on first start and DNS must resolve at that point.
| Domain | Record |
|---------------------|--------|
| cloud.ladkau.de | A → 217.154.207.148 |
| gitea.ladkau.de | A → 217.154.207.148 |
| nextcloud.ladkau.de | A → 217.154.207.148 |
| sso.ladkau.de | A → 217.154.207.148 |
| mail.ladkau.de | A → 217.154.207.148 |
| cr.ladkau.de | A → 217.154.207.148 |
| k8s.ladkau.de | A → 217.154.207.148 |
| cloud.ladkau.de | A → server IP |
| gitea.ladkau.de | A → server IP |
| nextcloud.ladkau.de | A → server IP |
| sso.ladkau.de | A → server IP |
| mail.ladkau.de | A → server IP |
| cr.ladkau.de | A → server IP |
| k8s.ladkau.de | A → server IP |
### 3. Create the vault and populate secrets
### 4. Create the vault and populate secrets
Create `ansible/group_vars/vault.yml` (gitignored) and encrypt it with Ansible Vault:
Create `ansible/group_vars/all/vault.yml` (gitignored) and encrypt it with Ansible Vault:
```bash
ansible-vault create ansible/group_vars/vault.yml
ansible-vault create ansible/group_vars/all/vault.yml
```
Populate all required secrets:
@@ -114,19 +126,34 @@ bash scripts/check-vault.sh
To edit the vault later:
```bash
ansible-vault edit ansible/group_vars/vault.yml
ansible-vault edit ansible/group_vars/all/vault.yml
```
### 4. Bootstrap the deploy user
Copies the bootstrap script to the server, runs it as root, and verifies the
deploy user can log in. Also runs `check-vault.sh` as a preflight check.
### 5. Bootstrap the deploy user
```bash
bash scripts/run-bootstrap.sh
```
### 5. Run the Ansible master playbook
The script runs the following steps in order:
**Preflight checks (local)**
1. Verifies all four key files exist under `keys/` (both root and deploy key pairs)
2. Verifies `scripts/bootstrap-deploy-user.sh` exists
3. Sets `chmod 600` on the private key files (SSH refuses keys with open permissions)
4. Runs `scripts/check-vault.sh` — decrypts the vault and confirms all 11 required
secrets are present and non-empty
**Remote actions**
5. Opens a test SSH connection as `root` to confirm the root key works
6. Copies `bootstrap-deploy-user.sh` to `/root/` on the server via `scp`
7. Executes it as root — creates the `deploy` user, grants passwordless sudo,
and installs `keys/notroot_cloud_ladkau_de.pub` as the only authorized key
8. Opens a test SSH connection as `deploy` to confirm the new user can log in
If any step fails the script exits immediately with a descriptive error message.
### 6. Run the Ansible master playbook
```bash
ansible-playbook -i ansible/inventory.ini ansible/site.yml --ask-vault-pass
@@ -160,7 +187,7 @@ ansible-playbook -i ansible/inventory.ini ansible/site.yml --tags <role> --ask-v
it to `false` in `vault.yml` so the setup wizard can create the admin account:
```bash
ansible-vault edit ansible/group_vars/vault.yml
ansible-vault edit ansible/group_vars/all/vault.yml
# add: gitea_disable_registration: false
ansible-playbook -i ansible/inventory.ini ansible/site.yml --tags gitea --ask-vault-pass
```
+4
View File
@@ -22,6 +22,10 @@ fi
echo "==> Adding '${DEPLOY_USER}' to sudo group"
usermod -aG sudo "${DEPLOY_USER}"
echo "==> Granting '${DEPLOY_USER}' passwordless sudo"
echo "${DEPLOY_USER} ALL=(ALL) NOPASSWD: ALL" > "/etc/sudoers.d/${DEPLOY_USER}"
chmod 440 "/etc/sudoers.d/${DEPLOY_USER}"
echo "==> Installing SSH authorized key"
SSH_DIR="/home/${DEPLOY_USER}/.ssh"
AUTH_KEYS="${SSH_DIR}/authorized_keys"
+1 -1
View File
@@ -7,7 +7,7 @@ set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
VAULT_FILE="${REPO_ROOT}/ansible/group_vars/vault.yml"
VAULT_FILE="${REPO_ROOT}/ansible/group_vars/all/vault.yml"
REQUIRED_KEYS=(
traefik_dashboard_users
+18 -2
View File
@@ -9,7 +9,12 @@ set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
SERVER="217.154.207.148"
SERVER=$(grep -oP '(?<=ansible_host=)\S+' "${REPO_ROOT}/ansible/inventory.ini" | head -1)
if [ -z "${SERVER}" ]; then
echo "ERROR: could not find ansible_host in ansible/inventory.ini" >&2
exit 1
fi
ROOT_KEY="${REPO_ROOT}/keys/root_cloud_ladkau_de"
DEPLOY_KEY="${REPO_ROOT}/keys/notroot_cloud_ladkau_de"
DEPLOY_PUBKEY="${REPO_ROOT}/keys/notroot_cloud_ladkau_de.pub"
@@ -37,6 +42,9 @@ if [ ! -f "${BOOTSTRAP_SCRIPT}" ]; then
exit 1
fi
# SSH private keys must not be group/world readable
chmod 600 "${ROOT_KEY}" "${DEPLOY_KEY}"
# --- Vault check ---
bash "${SCRIPT_DIR}/check-vault.sh"
@@ -46,10 +54,18 @@ SSH_OPTS="-o StrictHostKeyChecking=accept-new -o ConnectTimeout=10"
# --- Verify root access ---
echo "==> Verifying root SSH access to ${SERVER}"
if ! ssh ${SSH_OPTS} -i "${ROOT_KEY}" "root@${SERVER}" "echo ok" &>/dev/null; then
if ! ssh ${SSH_OPTS} -i "${ROOT_KEY}" "root@${SERVER}" "echo ok" 2>/tmp/ssh_root_err; then
echo "ERROR: cannot connect as root to ${SERVER}" >&2
echo " SSH error: $(cat /tmp/ssh_root_err)" >&2
echo "" >&2
echo "Possible causes:" >&2
echo " - Root public key not uploaded to the server (Strato control panel)" >&2
echo " - Wrong key file: ${ROOT_KEY}" >&2
echo " - Server not yet reachable (still booting?)" >&2
rm -f /tmp/ssh_root_err
exit 1
fi
rm -f /tmp/ssh_root_err
# --- Copy and run the bootstrap script ---