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:
+1
-1
@@ -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
|
||||
|
||||
|
||||
@@ -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,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
@@ -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
|
||||
```
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 ---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user