# Provisioning Runbook

Follow these steps to provision the server from scratch — whether setting it up
for the first time or reinstalling after a wipe.

After provisioning completes, follow `runbook-configuration.md` for first-run
setup of individual services.

## Prerequisites

### Local tools

- Ansible installed (`pip install ansible`)
- `passlib` installed (`pip install passlib`) — required for bcrypt/SHA-512 password hashing in templates
- `openssl` available (for generating secrets)

### SSH keys

Two SSH key pairs are required. Place them in the `keys/` directory (private keys
are gitignored; public keys are committed).

| File | Purpose |
|------|---------|
| `keys/root_cloud_ladkau_de` | Root access — used only during initial bootstrap |
| `keys/root_cloud_ladkau_de.pub` | Public counterpart |
| `keys/notroot_cloud_ladkau_de` | Deploy user — used by Ansible for all subsequent runs |
| `keys/notroot_cloud_ladkau_de.pub` | Public counterpart — installed on server by bootstrap |

Generate fresh key pairs if they do not already exist:

```bash
ssh-keygen -t ed25519 -f keys/root_cloud_ladkau_de -C "root@cloud.ladkau.de" -N ""
ssh-keygen -t ed25519 -f keys/notroot_cloud_ladkau_de -C "deploy@cloud.ladkau.de" -N ""
```

Upload the root public key to the server via the Strato control panel (or paste
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. 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:

```bash
ansible-galaxy collection install -r ansible/requirements.yml
```

Required collections:
- `community.general` — ufw, timezone modules
- `ansible.posix` — authorized_key module
- `community.docker` — docker_network, docker_compose_v2 modules

### 3. Configure DNS

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 → 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 |
| vault.ladkau.de     | A → server IP |
| dl.ladkau.de        | A → server IP |

### 4. Create the vault and populate secrets

Create `ansible/group_vars/all/vault.yml` (gitignored) and encrypt it with Ansible Vault:

```bash
ansible-vault create ansible/group_vars/all/vault.yml
```

Populate all required secrets. Ansible generates all password hashes at
deploy time — store plaintext values here (the vault is encrypted).

```yaml
# Traefik dashboard basic auth — generate with: openssl rand -hex 32
traefik_dashboard_users:
  - username: admin
    password: "your-password"

# Gitea — generate with: openssl rand -hex 32
gitea_db_password: ""
gitea_secret_key: ""
gitea_internal_token: ""

# Keycloak — generate with: openssl rand -hex 32
keycloak_db_password: ""
keycloak_admin_password: ""

# Nextcloud — generate with: openssl rand -hex 32
nextcloud_db_password: ""
nextcloud_admin_password: ""

# Roundcube — des_key must be exactly 24 characters: openssl rand -hex 12
roundcube_db_password: ""
roundcube_des_key: ""

# Container registry — generate with: openssl rand -hex 32
registry_users:
  - username: alice
    password: "your-password"

# Gitea Actions runners — token obtained after Gitea is running
# See step 3.4 of runbook-configuration.md; replace after Gitea admin account is created
gitea_runner_registration_token: "placeholder"

# 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

# Download server — SFTP upload key
# Generate: ssh-keygen -t ed25519 -f dl_deploy_key -N "" -C "gitea-actions"
# Paste the contents of dl_deploy_key.pub here; store dl_deploy_key as a Gitea Actions secret
dl_sftp_authorized_keys: |
  ssh-ed25519 AAAA...

# 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         # 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)
```

Verify all secrets are present and non-empty:

```bash
bash scripts/check-vault.sh
```

The script enforces three tiers:
- **Required scalars** — must be present and non-empty (all secrets above except
  `vaultwarden_sso_client_secret` and `dl_sftp_authorized_keys`)
- **Required lists** — must be present and contain at least one entry
  (`traefik_dashboard_users`, `registry_users`, `dovecot_users`)
- **Optional but validated when present** — if the key exists in the vault it
  must pass a format check:
  - `vaultwarden_sso_client_secret` — non-empty string (added after Keycloak is configured)
  - `dl_sftp_authorized_keys` — must begin with a recognised SSH public key prefix

To edit the vault later:

```bash
EDITOR=nano ansible-vault edit ansible/group_vars/all/vault.yml
```

### 5. Bootstrap the deploy user

```bash
bash scripts/run-bootstrap.sh
```

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, confirms all required
   secrets are present and non-empty, and validates optional secrets when present

**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
```

This runs all roles in order:

| #  | Role          | What it does |
|----|---------------|--------------|
| 1  | `base`        | OS hardening, deploy user, SSH config, ufw firewall, fail2ban |
| 2  | `docker`      | Docker Engine + Compose plugin, shared Traefik network |
| 3  | `traefik`     | Reverse proxy, automatic TLS via Let's Encrypt |
| 4  | `gitea`       | Self-hosted Git with PostgreSQL |
| 5  | `act_runner`  | Three Gitea Actions runners with Docker executor |
| 6  | `nextcloud`   | File storage with PostgreSQL, Redis, cron sidecar |
| 7  | `sso`         | Keycloak single sign-on with PostgreSQL |
| 8  | `mail`        | Roundcube webmail client with PostgreSQL |
| 9  | `registry`    | Docker Registry v2 with htpasswd auth |
| 10 | `k8s`         | Placeholder page at k8s.ladkau.de |
| 11 | `vaultwarden` | Vaultwarden password vault at vault.ladkau.de |
| 12 | `dl`          | Public download server at dl.ladkau.de — nginx HTTPS + SFTP upload |
| 13 | `dashboard`   | Public status dashboard at cloud.ladkau.de — service health and server stats |

> **Note:** The `act_runner` role requires `gitea_runner_registration_token` in the
> vault, which can only be obtained after Gitea is running and an admin account has
> been created. On a fresh provisioning run the role will fail if the token is
> absent. Either add a placeholder and redeploy with `--tags act_runner` after
> Gitea is configured (see step 3.4 of `runbook-configuration.md`), or skip the
> role on first run: `--skip-tags act_runner`.

To apply a single role:

```bash
ansible-playbook -i ansible/inventory.ini ansible/site.yml --tags <role> --ask-vault-pass
```

## Scripts reference

| Script | Purpose |
|--------|---------|
| `scripts/run-bootstrap.sh` | Copy and run the deploy user bootstrap on a fresh server |
| `scripts/bootstrap-deploy-user.sh` | Runs on the server as root — creates the deploy user |
| `scripts/check-vault.sh` | Verify required secrets are present; validate optional secrets (`vaultwarden_sso_client_secret`, `dl_sftp_authorized_keys`) when present |
| `scripts/check-services.sh` | Verify all service endpoints are reachable (run after provisioning) |
