Compare commits
2 commits
9ecc7a54fc
...
db70b4ba06
| Author | SHA1 | Date | |
|---|---|---|---|
| db70b4ba06 | |||
| 203bd5bf6e |
14 changed files with 220 additions and 18 deletions
13
.envrc
13
.envrc
|
|
@ -1,19 +1,30 @@
|
||||||
# Stack selection — set LINDERHOF_STACK before sourcing, or create a .stack file
|
# Stack selection — set LINDERHOF_STACK before sourcing, or create a .stack file
|
||||||
|
watch_file .stack
|
||||||
if [[ -z "${LINDERHOF_STACK:-}" ]]; then
|
if [[ -z "${LINDERHOF_STACK:-}" ]]; then
|
||||||
if [[ -f "$PWD/.stack" ]]; then
|
if [[ -f "$PWD/.stack" ]]; then
|
||||||
LINDERHOF_STACK="$(cat "$PWD/.stack")"
|
LINDERHOF_STACK="$(cat "$PWD/.stack")"
|
||||||
|
echo "linderhof: LINDERHOF_STACK is set to '$LINDERHOF_STACK'"
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ -z "${LINDERHOF_STACK:-}" ]]; then
|
if [[ -z "${LINDERHOF_STACK:-}" ]]; then
|
||||||
echo "linderhof: LINDERHOF_STACK is not set" >&2
|
echo "linderhof: LINDERHOF_STACK is not set" >&2
|
||||||
echo " set it in your environment, or run: echo <stack-name> > .stack" >&2
|
echo " new here? run: ./setup.sh" >&2
|
||||||
|
echo " existing stack? run: echo <stack-name> > .stack" >&2
|
||||||
else
|
else
|
||||||
export LINDERHOF_STACK
|
export LINDERHOF_STACK
|
||||||
export LINDERHOF_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/linderhof/$LINDERHOF_STACK"
|
export LINDERHOF_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/linderhof/$LINDERHOF_STACK"
|
||||||
export ANSIBLE_INVENTORY="$LINDERHOF_DIR/hosts.yml"
|
export ANSIBLE_INVENTORY="$LINDERHOF_DIR/hosts.yml"
|
||||||
export ANSIBLE_VAULT_PASSWORD_FILE="$LINDERHOF_DIR/vault-pass"
|
export ANSIBLE_VAULT_PASSWORD_FILE="$LINDERHOF_DIR/vault-pass"
|
||||||
|
|
||||||
|
# Extract HCLOUD_TOKEN from vault for hcloud CLI and Ansible modules
|
||||||
|
if [[ -f "$LINDERHOF_DIR/vault-pass" && -f "$LINDERHOF_DIR/group_vars/all/vault.yml" ]]; then
|
||||||
|
HCLOUD_TOKEN="$(ansible-vault view "$LINDERHOF_DIR/group_vars/all/vault.yml" \
|
||||||
|
--vault-password-file "$LINDERHOF_DIR/vault-pass" 2>/dev/null \
|
||||||
|
| grep '^hcloud_token:' | sed 's/^hcloud_token: *"\?\(.*\)$/\1/; s/"$//')"
|
||||||
|
export HCLOUD_TOKEN
|
||||||
|
fi
|
||||||
|
|
||||||
# Per-stack overrides: DOCKER_HOST, etc. — written by setup.sh
|
# Per-stack overrides: DOCKER_HOST, etc. — written by setup.sh
|
||||||
if [[ -f "$LINDERHOF_DIR/stack.env" ]]; then
|
if [[ -f "$LINDERHOF_DIR/stack.env" ]]; then
|
||||||
# shellcheck source=/dev/null
|
# shellcheck source=/dev/null
|
||||||
|
|
|
||||||
12
CLAUDE.md
12
CLAUDE.md
|
|
@ -37,17 +37,23 @@ Note: Inventory and vault password are set via `ANSIBLE_INVENTORY` and `ANSIBLE_
|
||||||
**Deployment Pattern:** Each service is deployed to `/srv/<service>/` on the target host with a `compose.yml` and environment files.
|
**Deployment Pattern:** Each service is deployed to `/srv/<service>/` on the target host with a `compose.yml` and environment files.
|
||||||
|
|
||||||
**Standalone Playbooks** (not in `site.yml`):
|
**Standalone Playbooks** (not in `site.yml`):
|
||||||
|
- `deploy.yml` - Full first-time deployment (chains provision → dns → storage_box → bootstrap → site)
|
||||||
- `provision.yml` - Provision a cloud VM (Hetzner)
|
- `provision.yml` - Provision a cloud VM (Hetzner)
|
||||||
- `dns.yml` - Manage DNS zones/records via Hetzner DNS API
|
- `dns.yml` - Manage DNS zones/records via Hetzner DNS API
|
||||||
- `bootstrap.yml` - First-time server setup (run once as root before site.yml)
|
- `bootstrap.yml` - First-time server setup (run once as root before site.yml)
|
||||||
- `dkim_sync.yml` - Fetch DKIM keys from mailserver and publish to DNS (run once after first mail deploy)
|
- `dkim_sync.yml` - Fetch DKIM keys from mailserver and publish to DNS (run once after first mail deploy)
|
||||||
|
- `storage_box.yml` - Create/configure a Hetzner Storage Box for restic backups (run once before enabling restic)
|
||||||
|
|
||||||
**Full deployment order** (fresh server):
|
**Full deployment order** (fresh server):
|
||||||
|
1. `deploy.yml` - runs all steps below in one shot (first-time only — bootstrap connects as root)
|
||||||
|
2. `dkim_sync.yml` - generate DKIM keys, write to stack config, publish to DNS (run once after mail is up)
|
||||||
|
|
||||||
|
**What `deploy.yml` runs internally:**
|
||||||
1. `provision.yml` - create server, auto-writes IP to hosts.yml and config.yml
|
1. `provision.yml` - create server, auto-writes IP to hosts.yml and config.yml
|
||||||
2. `dns.yml` - create DNS records
|
2. `dns.yml` - create DNS records
|
||||||
3. `bootstrap.yml` - users, SSH hardening, packages, Docker (connects as root)
|
3. `storage_box.yml` - generate SSH key, configure storage box, writes storagebox.yml to stack config
|
||||||
4. `site.yml` - deploy all services
|
4. `bootstrap.yml` - users, SSH hardening, packages, Docker (connects as root)
|
||||||
5. `dkim_sync.yml` - generate DKIM keys, write to stack config, publish to DNS
|
5. `site.yml` - deploy all services
|
||||||
|
|
||||||
**Playbook Execution Order** (via `site.yml`):
|
**Playbook Execution Order** (via `site.yml`):
|
||||||
1. networks.yml - Pre-create all Docker networks (must run before any service)
|
1. networks.yml - Pre-create all Docker networks (must run before any service)
|
||||||
|
|
|
||||||
13
README.md
13
README.md
|
|
@ -22,7 +22,7 @@ set `enable_<service>: false` in `config.yml` to disable any service — DNS rec
|
||||||
| image update alerts | `enable_diun` | on | [diun](https://github.com/crazy-max/diun) |
|
| image update alerts | `enable_diun` | on | [diun](https://github.com/crazy-max/diun) |
|
||||||
| intrusion prevention | `enable_fail2ban` | on | [fail2ban](https://github.com/fail2ban/fail2ban) |
|
| intrusion prevention | `enable_fail2ban` | on | [fail2ban](https://github.com/fail2ban/fail2ban) |
|
||||||
|
|
||||||
> **restic** is off by default — it requires a [Hetzner Storage Box](https://www.hetzner.com/storage/storage-box/) for its backup target. enable it and configure `restic_repository` in `config.yml` once you have one.
|
> **restic** is off by default — it requires a [Hetzner Storage Box](https://www.hetzner.com/storage/storage-box/). run `ansible-playbook playbooks/storage_box.yml` once to create the box, generate an SSH key pair, and install it — then set `enable_restic: true` and re-run `site.yml`.
|
||||||
|
|
||||||
|
|
||||||
## what you need
|
## what you need
|
||||||
|
|
@ -33,6 +33,7 @@ set `enable_<service>: false` in `config.yml` to disable any service — DNS rec
|
||||||
- `ansible` and `ansible-galaxy`
|
- `ansible` and `ansible-galaxy`
|
||||||
- `direnv` (optional but recommended — loads `.envrc` automatically)
|
- `direnv` (optional but recommended — loads `.envrc` automatically)
|
||||||
- `ssh-keygen`, `openssl`, `envsubst` (standard on most systems)
|
- `ssh-keygen`, `openssl`, `envsubst` (standard on most systems)
|
||||||
|
- `sshpass` (only needed for `storage_box.yml`)
|
||||||
|
|
||||||
if you already have a server with SSH access and passwordless sudo, you can skip provisioning and jump straight to [deploy](#deploy).
|
if you already have a server with SSH access and passwordless sudo, you can skip provisioning and jump straight to [deploy](#deploy).
|
||||||
|
|
||||||
|
|
@ -45,7 +46,7 @@ run the interactive setup wizard:
|
||||||
./setup.sh
|
./setup.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
it walks you through: stack name, SSH key, admin username, server hostname, domain, Hetzner API token, and generates all secrets. config is written to `$XDG_CONFIG_HOME/linderhof/<stack>/` and won't overwrite existing files.
|
it walks you through: stack name, SSH key, admin username, server hostname, domain, Hetzner API token, storage box name and password, and generates all secrets. config is written to `$XDG_CONFIG_HOME/linderhof/<stack>/` and won't overwrite existing files.
|
||||||
|
|
||||||
activate the stack and review the generated config:
|
activate the stack and review the generated config:
|
||||||
|
|
||||||
|
|
@ -67,9 +68,17 @@ ansible-galaxy collection install -r requirements.yml
|
||||||
|
|
||||||
full deployment order for a fresh server:
|
full deployment order for a fresh server:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ansible-playbook playbooks/deploy.yml # provision → dns → storage_box → bootstrap → site (all-in-one)
|
||||||
|
ansible-playbook playbooks/dkim_sync.yml # generate DKIM keys and publish to DNS (run once after mail is up)
|
||||||
|
```
|
||||||
|
|
||||||
|
or step by step:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
ansible-playbook playbooks/provision.yml # create server, writes IP to stack config
|
ansible-playbook playbooks/provision.yml # create server, writes IP to stack config
|
||||||
ansible-playbook playbooks/dns.yml # create DNS zones and records
|
ansible-playbook playbooks/dns.yml # create DNS zones and records
|
||||||
|
ansible-playbook playbooks/storage_box.yml # create storage box and install SSH key (if using restic)
|
||||||
ansible-playbook playbooks/site.yml --tags bootstrap # users, SSH hardening, packages, Docker
|
ansible-playbook playbooks/site.yml --tags bootstrap # users, SSH hardening, packages, Docker
|
||||||
ansible-playbook playbooks/site.yml # deploy all services
|
ansible-playbook playbooks/site.yml # deploy all services
|
||||||
ansible-playbook playbooks/dkim_sync.yml # generate DKIM keys and publish to DNS
|
ansible-playbook playbooks/dkim_sync.yml # generate DKIM keys and publish to DNS
|
||||||
|
|
|
||||||
|
|
@ -137,11 +137,17 @@ grafana_root_url: "https://{{ grafana_domain }}"
|
||||||
# Restic (encrypted backups)
|
# Restic (encrypted backups)
|
||||||
# ============================================================
|
# ============================================================
|
||||||
restic_backend_type: "sftp"
|
restic_backend_type: "sftp"
|
||||||
|
# Storage box name in Hetzner Cloud (https://console.hetzner.cloud)
|
||||||
|
restic_storagebox_name: "$restic_storagebox_name"
|
||||||
|
# To create a new storage box via storage_box.yml (rather than adopting an existing one):
|
||||||
|
# restic_storagebox_type: bx11
|
||||||
|
# restic_storagebox_location: $hcloud_location
|
||||||
|
# The following are written automatically by storage_box.yml — do not edit manually
|
||||||
# restic_host: "uXXXXXX.your-storagebox.de"
|
# restic_host: "uXXXXXX.your-storagebox.de"
|
||||||
# restic_user: uXXXXXX
|
# restic_user: uXXXXXX
|
||||||
# restic_ssh_port: 23
|
# restic_ssh_port: 23
|
||||||
# restic_remote_path: "backups/$server_name"
|
# restic_remote_path: "backups/$server_name"
|
||||||
# restic_ssh_key: "/root/.ssh/island_restic_backup"
|
# restic_ssh_key: "/root/.ssh/restic_backup"
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# GoAccess (web analytics)
|
# GoAccess (web analytics)
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,7 @@ diun_email_password: "$notifications_mail_password"
|
||||||
# restic
|
# restic
|
||||||
# password generated with: openssl rand -base64 32
|
# password generated with: openssl rand -base64 32
|
||||||
restic_password: "$restic_password"
|
restic_password: "$restic_password"
|
||||||
|
restic_storagebox_password: "$restic_storagebox_password"
|
||||||
|
|
||||||
# fail2ban (optional — IPs/CIDRs to whitelist)
|
# fail2ban (optional — IPs/CIDRs to whitelist)
|
||||||
# fail2ban_ignoreip: "your-home-ip/32"
|
# fail2ban_ignoreip: "your-home-ip/32"
|
||||||
|
|
|
||||||
27
playbooks/deploy.yml
Normal file
27
playbooks/deploy.yml
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
---
|
||||||
|
# Full first-time deployment — provisions and deploys everything in one shot.
|
||||||
|
# Usage: ansible-playbook playbooks/deploy.yml
|
||||||
|
#
|
||||||
|
# Prerequisites: run setup.sh first, then review config.yml, vault.yml, dns.yml
|
||||||
|
#
|
||||||
|
# This playbook is intended for initial deployments only. After the first run,
|
||||||
|
# bootstrap will fail (root SSH is disabled) — use site.yml for subsequent deploys.
|
||||||
|
#
|
||||||
|
# dkim_sync.yml is intentionally excluded: it requires the mail server to be
|
||||||
|
# fully running and keys generated. Run it manually after confirming mail is up:
|
||||||
|
# ansible-playbook playbooks/dkim_sync.yml
|
||||||
|
|
||||||
|
- import_playbook: provision.yml
|
||||||
|
- import_playbook: dns.yml
|
||||||
|
- import_playbook: storage_box.yml
|
||||||
|
|
||||||
|
# Refresh inventory so the newly provisioned server IP is visible to subsequent plays
|
||||||
|
- name: Refresh inventory
|
||||||
|
hosts: localhost
|
||||||
|
connection: local
|
||||||
|
gather_facts: false
|
||||||
|
tasks:
|
||||||
|
- meta: refresh_inventory
|
||||||
|
|
||||||
|
- import_playbook: bootstrap.yml
|
||||||
|
- import_playbook: site.yml
|
||||||
25
playbooks/storage_box.yml
Normal file
25
playbooks/storage_box.yml
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
---
|
||||||
|
# Configure a Hetzner Storage Box for restic backups.
|
||||||
|
# Run once before the first restic deployment (enable_restic: true).
|
||||||
|
#
|
||||||
|
# Prerequisites:
|
||||||
|
# - restic_storagebox_id set in config.yml
|
||||||
|
# - hetzner_robot_user / hetzner_robot_password set in vault.yml
|
||||||
|
#
|
||||||
|
# What it does:
|
||||||
|
# 1. Generates an SSH key pair in LINDERHOF_DIR (skips if already present)
|
||||||
|
# 2. Fetches storage box details from Robot API (derives restic_user / restic_host)
|
||||||
|
# 3. Enables SSH access on the storage box
|
||||||
|
# 4. Uploads the public key (replaces any existing key with the same label)
|
||||||
|
# 5. Writes LINDERHOF_DIR/group_vars/all/storagebox.yml — loaded automatically
|
||||||
|
# by Ansible on subsequent runs, no manual config edits required
|
||||||
|
|
||||||
|
- name: Configure Hetzner Storage Box for restic backups
|
||||||
|
hosts: localhost
|
||||||
|
connection: local
|
||||||
|
gather_facts: false
|
||||||
|
become: false
|
||||||
|
|
||||||
|
roles:
|
||||||
|
- role: storage_box
|
||||||
|
tags: storage_box
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
---
|
---
|
||||||
collections:
|
collections:
|
||||||
- name: hetzner.hcloud
|
- name: hetzner.hcloud
|
||||||
version: ">=6.0.0"
|
version: ">=6.7.0"
|
||||||
- name: ansible.posix
|
- name: ansible.posix
|
||||||
|
|
|
||||||
|
|
@ -81,6 +81,8 @@
|
||||||
</main>
|
</main>
|
||||||
<footer>
|
<footer>
|
||||||
powered by <a href="https://codeberg.org/opennomad/linderhof" target="_blank" rel="noopener">linderhof</a>
|
powered by <a href="https://codeberg.org/opennomad/linderhof" target="_blank" rel="noopener">linderhof</a>
|
||||||
|
·
|
||||||
|
design by <a href="https://github.com/YC/coming-soon" target="_blank" rel="noopener">steven tang</a>
|
||||||
</footer>
|
</footer>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,11 @@ restic_backend_type: "sftp"
|
||||||
restic_password: ""
|
restic_password: ""
|
||||||
# restic_repo: set explicitly when restic_backend_type is not 'sftp'
|
# restic_repo: set explicitly when restic_backend_type is not 'sftp'
|
||||||
|
|
||||||
|
# SFTP backend: path to the SSH private key on the controller and on the target server
|
||||||
|
# Both are written by storage_box.yml — no need to set these manually
|
||||||
|
restic_local_key_path: "{{ lookup('env', 'LINDERHOF_DIR') }}/restic_backup"
|
||||||
|
restic_ssh_key: /root/.ssh/restic_backup
|
||||||
|
|
||||||
restic_backup_paths: >-
|
restic_backup_paths: >-
|
||||||
{{
|
{{
|
||||||
['/etc/letsencrypt', '/srv/caddy']
|
['/etc/letsencrypt', '/srv/caddy']
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
---
|
---
|
||||||
- name: Deploy Restic SSH key
|
- name: Deploy Restic SSH key
|
||||||
ansible.builtin.copy:
|
ansible.builtin.copy:
|
||||||
src: restic_backup # local path in your playbook repo
|
src: "{{ restic_local_key_path }}"
|
||||||
dest: "{{ restic_ssh_key }}" # e.g. /root/.ssh/restic_backup
|
dest: "{{ restic_ssh_key }}"
|
||||||
owner: root
|
owner: root
|
||||||
group: root
|
group: root
|
||||||
mode: '0600'
|
mode: '0600'
|
||||||
|
|
|
||||||
20
roles/storage_box/defaults/main.yml
Normal file
20
roles/storage_box/defaults/main.yml
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
---
|
||||||
|
# Storage box name used to identify (or create) the box
|
||||||
|
restic_storagebox_name: "{{ server_name }}-backup"
|
||||||
|
|
||||||
|
# Set these only when creating a new storage box from scratch.
|
||||||
|
# Leave unset if the box already exists (identified by restic_storagebox_name above).
|
||||||
|
# restic_storagebox_type: bx11
|
||||||
|
# restic_storagebox_location: fsn1
|
||||||
|
|
||||||
|
# SSH port for Hetzner Storage Boxes
|
||||||
|
restic_ssh_port: 23
|
||||||
|
|
||||||
|
# Path where the private key is stored on the Ansible controller (per-stack)
|
||||||
|
restic_local_key_path: "{{ lookup('env', 'LINDERHOF_DIR') }}/restic_backup"
|
||||||
|
|
||||||
|
# Path on the target server where the private key will be deployed
|
||||||
|
restic_ssh_key: /root/.ssh/restic_backup
|
||||||
|
|
||||||
|
# Remote path on the storage box for this server's backups
|
||||||
|
restic_remote_path: "backups/{{ server_name }}"
|
||||||
86
roles/storage_box/tasks/main.yml
Normal file
86
roles/storage_box/tasks/main.yml
Normal file
|
|
@ -0,0 +1,86 @@
|
||||||
|
---
|
||||||
|
- name: Generate restic SSH key pair
|
||||||
|
ansible.builtin.command:
|
||||||
|
cmd: >-
|
||||||
|
ssh-keygen -t ed25519
|
||||||
|
-f {{ restic_local_key_path }}
|
||||||
|
-N ""
|
||||||
|
-C "restic-{{ server_name }}"
|
||||||
|
creates: "{{ restic_local_key_path }}"
|
||||||
|
check_mode: false
|
||||||
|
|
||||||
|
- name: Check if SSH public key exists
|
||||||
|
ansible.builtin.stat:
|
||||||
|
path: "{{ restic_local_key_path }}.pub"
|
||||||
|
register: ssh_pub_key_stat
|
||||||
|
|
||||||
|
- name: Read SSH public key
|
||||||
|
ansible.builtin.slurp:
|
||||||
|
src: "{{ restic_local_key_path }}.pub"
|
||||||
|
register: ssh_pub_key_raw
|
||||||
|
when: ssh_pub_key_stat.stat.exists
|
||||||
|
|
||||||
|
- name: Set public key fact
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
restic_ssh_pub_key: "{{ ssh_pub_key_raw.content | b64decode | trim }}"
|
||||||
|
when: ssh_pub_key_stat.stat.exists
|
||||||
|
|
||||||
|
- name: Configure Hetzner Storage Box
|
||||||
|
hetzner.hcloud.storage_box:
|
||||||
|
name: "{{ restic_storagebox_name }}"
|
||||||
|
storage_box_type: "{{ restic_storagebox_type | default(omit) }}"
|
||||||
|
location: "{{ restic_storagebox_location | default(omit) }}"
|
||||||
|
password: "{{ restic_storagebox_password }}"
|
||||||
|
api_token: "{{ hcloud_token }}"
|
||||||
|
access_settings:
|
||||||
|
ssh_enabled: true
|
||||||
|
state: present
|
||||||
|
register: storagebox_result
|
||||||
|
when: ssh_pub_key_stat.stat.exists
|
||||||
|
|
||||||
|
- name: Check SSH key auth on Storage Box
|
||||||
|
ansible.builtin.shell: |
|
||||||
|
echo "bye" | sftp -i {{ restic_local_key_path }} \
|
||||||
|
-o BatchMode=yes -o StrictHostKeyChecking=no \
|
||||||
|
-P 23 \
|
||||||
|
{{ storagebox_result.hcloud_storage_box.username }}@{{ storagebox_result.hcloud_storage_box.server }}
|
||||||
|
register: ssh_key_check
|
||||||
|
failed_when: false
|
||||||
|
changed_when: false
|
||||||
|
when: ssh_pub_key_stat.stat.exists
|
||||||
|
|
||||||
|
- name: Install SSH public key on Storage Box
|
||||||
|
ansible.builtin.shell: |
|
||||||
|
cat {{ restic_local_key_path }}.pub | \
|
||||||
|
sshpass -p "{{ restic_storagebox_password }}" \
|
||||||
|
ssh -o StrictHostKeyChecking=no -p 23 \
|
||||||
|
{{ storagebox_result.hcloud_storage_box.username }}@{{ storagebox_result.hcloud_storage_box.server }} \
|
||||||
|
install-ssh-key
|
||||||
|
no_log: true
|
||||||
|
when: ssh_pub_key_stat.stat.exists and ssh_key_check.rc != 0
|
||||||
|
|
||||||
|
- name: Write storagebox.yml to stack config directory
|
||||||
|
ansible.builtin.copy:
|
||||||
|
content: |
|
||||||
|
---
|
||||||
|
# Storage box config — written automatically by storage_box.yml, do not edit manually
|
||||||
|
restic_user: {{ storagebox_result.hcloud_storage_box.username }}
|
||||||
|
restic_host: {{ storagebox_result.hcloud_storage_box.server }}
|
||||||
|
restic_ssh_port: {{ restic_ssh_port }}
|
||||||
|
restic_remote_path: {{ restic_remote_path }}
|
||||||
|
restic_ssh_key: {{ restic_ssh_key }}
|
||||||
|
restic_local_key_path: {{ restic_local_key_path }}
|
||||||
|
dest: "{{ lookup('env', 'ANSIBLE_INVENTORY') | dirname }}/group_vars/all/storagebox.yml"
|
||||||
|
mode: "0600"
|
||||||
|
when: ssh_pub_key_stat.stat.exists
|
||||||
|
|
||||||
|
- name: Print connection info
|
||||||
|
ansible.builtin.debug:
|
||||||
|
msg:
|
||||||
|
- "Storage box configured successfully"
|
||||||
|
- "User: {{ storagebox_result.hcloud_storage_box.username }}"
|
||||||
|
- "Host: {{ storagebox_result.hcloud_storage_box.server }}"
|
||||||
|
- "Remote path: {{ restic_remote_path }}"
|
||||||
|
- "Local key: {{ restic_local_key_path }}"
|
||||||
|
- "Next: set enable_restic: true and run site.yml or restic.yml"
|
||||||
|
when: ssh_pub_key_stat.stat.exists
|
||||||
20
setup.sh
20
setup.sh
|
|
@ -101,7 +101,12 @@ if [[ -z "$hcloud_token" ]]; then
|
||||||
warn "no Hetzner token provided — add it to vault.yml manually if needed"
|
warn "no Hetzner token provided — add it to vault.yml manually if needed"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
export admin_user server_name server_ip domain hcloud_token
|
echo
|
||||||
|
info "configure restic backups (optional — leave blank to skip)"
|
||||||
|
prompt restic_storagebox_name "Storage box name" "${server_name}-backup"
|
||||||
|
prompt_secret restic_storagebox_password "Storage box password (leave blank to skip)"
|
||||||
|
|
||||||
|
export admin_user server_name server_ip domain hcloud_token restic_storagebox_name restic_storagebox_password
|
||||||
export ssh_key_pub="${ssh_key_path}.pub"
|
export ssh_key_pub="${ssh_key_path}.pub"
|
||||||
|
|
||||||
echo
|
echo
|
||||||
|
|
@ -118,7 +123,7 @@ info "generating secrets..."
|
||||||
export root_password admin_password
|
export root_password admin_password
|
||||||
export admin_mail_password notifications_mail_password git_mail_password
|
export admin_mail_password notifications_mail_password git_mail_password
|
||||||
export grafana_admin_password rspamd_web_password goaccess_password rainloop_admin_password radicale_password
|
export grafana_admin_password rspamd_web_password goaccess_password rainloop_admin_password radicale_password
|
||||||
export tuwunel_registration_token restic_password
|
export tuwunel_registration_token restic_password restic_storagebox_password
|
||||||
export forgejo_secret_key forgejo_internal_token forgejo_jwt_secret
|
export forgejo_secret_key forgejo_internal_token forgejo_jwt_secret
|
||||||
|
|
||||||
root_password=$(openssl rand -base64 32)
|
root_password=$(openssl rand -base64 32)
|
||||||
|
|
@ -176,7 +181,7 @@ if [[ -f "$CONFIG" ]]; then
|
||||||
warn "config.yml already exists — skipping (not overwriting)"
|
warn "config.yml already exists — skipping (not overwriting)"
|
||||||
else
|
else
|
||||||
info "writing config.yml..."
|
info "writing config.yml..."
|
||||||
envsubst '$admin_user $server_name $server_ip $domain $ssh_key_pub $stack_name' \
|
envsubst '$admin_user $server_name $server_ip $domain $ssh_key_pub $stack_name $restic_storagebox_name' \
|
||||||
< "$TEMPLATES/config.yml.setup" > "$CONFIG"
|
< "$TEMPLATES/config.yml.setup" > "$CONFIG"
|
||||||
ok "config.yml created"
|
ok "config.yml created"
|
||||||
fi
|
fi
|
||||||
|
|
@ -242,8 +247,7 @@ echo "Next steps:"
|
||||||
echo " 1. Review $CONFIG"
|
echo " 1. Review $CONFIG"
|
||||||
echo " 2. Review $VAULT (ansible-vault edit)"
|
echo " 2. Review $VAULT (ansible-vault edit)"
|
||||||
echo " 3. Review $DNS_CONFIG"
|
echo " 3. Review $DNS_CONFIG"
|
||||||
echo " 4. Provision a server: ansible-playbook playbooks/provision.yml"
|
echo " 4. Deploy: ansible-playbook playbooks/deploy.yml"
|
||||||
echo " 5. Update DNS: ansible-playbook playbooks/dns.yml"
|
echo ""
|
||||||
echo " 6. Bootstrap server: ansible-playbook playbooks/site.yml --tags bootstrap"
|
echo " If mail is enabled, sync DKIM keys once the server is up:"
|
||||||
echo " 7. Deploy: ansible-playbook playbooks/site.yml"
|
echo " ansible-playbook playbooks/dkim_sync.yml"
|
||||||
echo " 8. Sync DKIM keys to DNS: ansible-playbook playbooks/dkim_sync.yml"
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue