Set up repo structure and implement base role

- Document architecture decisions in README (Ansible + Docker Compose + Traefik)
- Scaffold Ansible layout: inventory, site.yml, group_vars, 9 service roles
- Implement base role: package install, deploy user, SSH hardening,
  UFW firewall (22/80/443), fail2ban, unattended-upgrades
- Add reinstall runbook in docs/runbook.md
- Add ansible/requirements.yml for community.general and ansible.posix
This commit is contained in:
ml
2026-06-27 15:02:21 +02:00
parent 694d6d5526
commit 6115d9d48f
18 changed files with 385 additions and 0 deletions
+75
View File
@@ -0,0 +1,75 @@
Info:
--
Server:
Strato STRATO VPS Linux VC4-8
Order Number: 9478462
Customer Number: 73171334
Customer Login: https://www.strato.de/apps/CustomerService
Cores: 4 Cores
RAM: 8 GB RAM
Storage: 240 HDD
IPv4: 217.154.207.148
IPv6: 2a01:239:35b:c400::1
OS: Ubuntu 24.04 LTS
DNS Names:
cloud.ladkau.de
gitea.ladkau.de
nextcloud.ladkau.de
sso.ladkau.de
mail.ladkau.de
cr.ladkau.de
k8s.ladkau.de
Purpose:
--
This repository tracks the complete server configuration so the server can be
fully reinstalled from scratch using only this repo. Every configuration change
must be committed here. The git history serves as the change log.
Architecture:
--
Provisioning: Ansible
Ansible playbooks configure the OS and deploy all services. Roles are
idempotent — re-running them brings the server back to the desired state
without side effects. The master playbook is ansible/site.yml.
Service runtime: Docker Compose
Each service runs as a Docker Compose stack. Compose files live inside
their respective Ansible roles (roles/<service>/files/docker-compose.yml).
This keeps service definition and deployment config together.
Reverse proxy / TLS: Traefik
Traefik is the single entry point for all HTTP/HTTPS traffic. It runs as
a Docker Compose service and routes to other containers via Docker labels.
TLS certificates are issued automatically via Let's Encrypt (ACME).
Repo layout:
--
ansible/
inventory.ini # host address and connection vars
site.yml # master playbook — runs all roles in order
group_vars/all.yml # shared variables (domains, image versions, ...)
roles/
base/ # OS hardening, non-root user, SSH, ufw firewall
docker/ # Docker Engine + Compose plugin install
traefik/ # reverse proxy, TLS termination
gitea/ # self-hosted Git
nextcloud/ # file storage and collaboration
sso/ # Single Sign-On
mail/ # mail server
registry/ # container registry (cr.ladkau.de)
k8s/ # k3s Kubernetes node
docs/
runbook.md # step-by-step reinstall instructions
keys/
*.pub # public SSH keys (private keys are gitignored)
SSH keys:
--
root_cloud_ladkau_de — root access (initial setup only)
notroot_cloud_ladkau_de — non-root deploy user (used by Ansible)
+22
View File
@@ -0,0 +1,22 @@
---
# Domains
domain_base: ladkau.de
domain_cloud: "cloud.{{ domain_base }}"
domain_gitea: "gitea.{{ domain_base }}"
domain_nextcloud: "nextcloud.{{ domain_base }}"
domain_sso: "sso.{{ domain_base }}"
domain_mail: "mail.{{ domain_base }}"
domain_registry: "cr.{{ domain_base }}"
domain_k8s: "k8s.{{ domain_base }}"
# Let's Encrypt
acme_email: matthias.ladkau@gmail.com
# Non-root deploy user created by the base role
deploy_user: deploy
# System timezone
timezone: Europe/Berlin
# Docker network shared by all services and Traefik
traefik_network: traefik_public
+2
View File
@@ -0,0 +1,2 @@
[cloud]
cloud.ladkau.de ansible_user=deploy ansible_ssh_private_key_file=../keys/notroot_cloud_ladkau_de
+4
View File
@@ -0,0 +1,4 @@
---
collections:
- name: community.general # ufw, timezone modules
- name: ansible.posix # authorized_key module
+2
View File
@@ -0,0 +1,2 @@
APT::Periodic::Update-Package-Lists "1";
APT::Periodic::Unattended-Upgrade "1";
+5
View File
@@ -0,0 +1,5 @@
---
- name: Restart sshd
ansible.builtin.service:
name: ssh
state: restarted
+134
View File
@@ -0,0 +1,134 @@
---
# --- Packages ---
- name: Update apt cache
ansible.builtin.apt:
update_cache: true
cache_valid_time: 3600
tags: base
- name: Upgrade all packages
ansible.builtin.apt:
upgrade: dist
tags: base
- name: Install base packages
ansible.builtin.apt:
name:
- curl
- git
- vim
- htop
- ufw
- fail2ban
- unattended-upgrades
- apt-listchanges
state: present
tags: base
# --- System ---
- name: Set timezone
community.general.timezone:
name: "{{ timezone }}"
tags: base
# --- Deploy user ---
- name: Create deploy user
ansible.builtin.user:
name: "{{ deploy_user }}"
shell: /bin/bash
create_home: true
groups: sudo
append: true
state: present
tags: base
- name: Add SSH authorized key for deploy user
ansible.posix.authorized_key:
user: "{{ deploy_user }}"
state: present
key: "{{ lookup('file', playbook_dir + '/../keys/notroot_cloud_ladkau_de.pub') }}"
tags: base
- name: Allow deploy user passwordless sudo
ansible.builtin.copy:
dest: /etc/sudoers.d/{{ deploy_user }}
content: "{{ deploy_user }} ALL=(ALL) NOPASSWD:ALL\n"
mode: "0440"
validate: visudo -cf %s
tags: base
# --- SSH hardening ---
- name: Deploy hardened sshd_config
ansible.builtin.template:
src: sshd_config.j2
dest: /etc/ssh/sshd_config
owner: root
group: root
mode: "0600"
validate: sshd -t -f %s
notify: Restart sshd
tags: base
# --- Firewall ---
- name: Set UFW default incoming policy to deny
community.general.ufw:
direction: incoming
policy: deny
tags: base
- name: Set UFW default outgoing policy to allow
community.general.ufw:
direction: outgoing
policy: allow
tags: base
- name: Allow SSH (22/tcp)
community.general.ufw:
rule: allow
port: "22"
proto: tcp
tags: base
- name: Allow HTTP (80/tcp)
community.general.ufw:
rule: allow
port: "80"
proto: tcp
tags: base
- name: Allow HTTPS (443/tcp)
community.general.ufw:
rule: allow
port: "443"
proto: tcp
tags: base
- name: Enable UFW
community.general.ufw:
state: enabled
tags: base
# --- Automatic security updates ---
- name: Enable unattended-upgrades
ansible.builtin.copy:
src: 20auto-upgrades
dest: /etc/apt/apt.conf.d/20auto-upgrades
owner: root
group: root
mode: "0644"
tags: base
# --- Fail2ban ---
- name: Enable and start fail2ban
ansible.builtin.service:
name: fail2ban
state: started
enabled: true
tags: base
@@ -0,0 +1,23 @@
# Managed by Ansible — do not edit manually
Port 22
Protocol 2
# Authentication
PermitRootLogin no
PasswordAuthentication no
ChallengeResponseAuthentication no
PubkeyAuthentication yes
AuthorizedKeysFile .ssh/authorized_keys
# Only allow the deploy user over SSH
AllowUsers {{ deploy_user }}
# Misc hardening
X11Forwarding no
PrintMotd no
MaxAuthTries 3
LoginGraceTime 30
UsePAM yes
AcceptEnv LANG LC_*
Subsystem sftp /usr/lib/openssh/sftp-server
+2
View File
@@ -0,0 +1,2 @@
---
# TODO: implement docker role
+2
View File
@@ -0,0 +1,2 @@
---
# TODO: implement gitea role
+2
View File
@@ -0,0 +1,2 @@
---
# TODO: implement k8s role
+2
View File
@@ -0,0 +1,2 @@
---
# TODO: implement mail role
+2
View File
@@ -0,0 +1,2 @@
---
# TODO: implement nextcloud role
+2
View File
@@ -0,0 +1,2 @@
---
# TODO: implement registry role
+2
View File
@@ -0,0 +1,2 @@
---
# TODO: implement sso role
+2
View File
@@ -0,0 +1,2 @@
---
# TODO: implement traefik role
+14
View File
@@ -0,0 +1,14 @@
---
- name: Full server provisioning
hosts: cloud
become: true
roles:
- base
- docker
- traefik
- gitea
- nextcloud
- sso
- mail
- registry
- k8s
+88
View File
@@ -0,0 +1,88 @@
# Reinstall Runbook
Follow these steps to provision a fresh server from scratch.
## Prerequisites
On your local machine:
- Ansible installed (`pip install ansible`)
- SSH access to the server as root using `keys/root_cloud_ladkau_de`
## Steps
### 1. 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
### 2. Initial root login
Connect as root and verify the server is reachable:
```
ssh -i keys/root_cloud_ladkau_de root@217.154.207.148
```
### 3. Create the deploy user (one-time, manual)
On the server as root:
```bash
adduser deploy
usermod -aG sudo deploy
mkdir -p /home/deploy/.ssh
cat >> /home/deploy/.ssh/authorized_keys <<'EOF'
<paste contents of keys/notroot_cloud_ladkau_de.pub>
EOF
chown -R deploy:deploy /home/deploy/.ssh
chmod 700 /home/deploy/.ssh
chmod 600 /home/deploy/.ssh/authorized_keys
```
### 4. Run the Ansible master playbook
From the repo root:
```bash
ansible-playbook -i ansible/inventory.ini ansible/site.yml
```
This runs all roles in order:
1. `base` — OS hardening, SSH config, ufw firewall
2. `docker` — Docker Engine + Compose plugin
3. `traefik` — reverse proxy, TLS via Let's Encrypt
4. `gitea` — self-hosted Git
5. `nextcloud` — file storage
6. `sso` — Single Sign-On
7. `mail` — mail server
8. `registry` — container registry
9. `k8s` — k3s node
### 5. DNS
Ensure the following DNS A records point to `217.154.207.148` before running:
- cloud.ladkau.de
- gitea.ladkau.de
- nextcloud.ladkau.de
- sso.ladkau.de
- mail.ladkau.de
- cr.ladkau.de
- k8s.ladkau.de
Traefik will attempt ACME certificate issuance on first start; DNS must resolve first.
## Re-running after changes
The playbook is idempotent. To apply a single role only:
```bash
ansible-playbook -i ansible/inventory.ini ansible/site.yml --tags <role>
```