Add Proxmox host role, WireGuard VPN, and public Gitea via Traefik HTTPS
Ansible Lint / lint (push) Successful in 4s
Ansible Lint / lint (push) Successful in 4s
Proxmox host role: - Admin user setup, SSH hardening (port 2222, no root login) - claude-code SFTP chroot user - Unattended upgrades, DuckDNS dynamic DNS updater (vault-encrypted token) - WireGuard VPN server (10.10.10.0/24) with Fedora client registered - Proxmox firewall management via pvesh API (idempotent Python script) Public Gitea exposure: - Traefik: add HTTPS entrypoint, Let's Encrypt ACME (HTTP-01), StripPrefix middleware for /git subpath, HTTP→HTTPS redirect - Gitea ROOT_URL updated to https://adyrem.duckdns.org/git/ - Pi-hole split DNS: adyrem.duckdns.org → 10.10.1.3 for internal hairpin bypass SSH config fixes: - StrictHostKeyChecking no → accept-new across all hosts - Proxmox jump host: port 2222, user adyrem (root login disabled) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,2 +1,2 @@
|
||||
ansible_user: adyrem
|
||||
ansible_ssh_common_args: "-o ProxyJump=root@192.168.1.10 -o StrictHostKeyChecking=no"
|
||||
ansible_ssh_common_args: "-o ProxyJump=root@192.168.1.10 -o StrictHostKeyChecking=accept-new"
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
ansible_user: adyrem
|
||||
ansible_become: true
|
||||
ansible_become_method: sudo
|
||||
ansible_port: 2222
|
||||
|
||||
ssh_port: 2222
|
||||
|
||||
claude_code_ssh_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINgCFi4cju1/dmyXkVtO1azFt9VvWNoYjuUjcw7IlJdC claude-code@homelab"
|
||||
|
||||
duckdns_domain: adyrem
|
||||
duckdns_token: "{{ vault_duckdns_token }}"
|
||||
|
||||
wireguard_port: 51820
|
||||
wireguard_peers:
|
||||
- name: fedora
|
||||
public_key: "ivtchk9pxEwYwusMzmn7Uq89LFV3uB1I8iYto0EJuy4="
|
||||
allowed_ips: 10.10.10.2/32
|
||||
@@ -0,0 +1,8 @@
|
||||
vault_duckdns_token: !vault |
|
||||
$ANSIBLE_VAULT;1.1;AES256
|
||||
34386432626362373134333765386630616466383661623463363465353135633763623364366166
|
||||
3233343837653638613262663537383630376635323031350a653464653166396165643136643163
|
||||
66376663666332303337306165313337303261373033363834373031303335383839383139663461
|
||||
3036356435613435660a323432303964326362646462653139656664653439346331323261636436
|
||||
62343135373431326231333736613836366238663064623163336636393663633862383439363333
|
||||
6530326532373062636237613765353039373164333063613331
|
||||
@@ -34,3 +34,8 @@ all:
|
||||
traefik_servers:
|
||||
hosts:
|
||||
traefik:
|
||||
|
||||
proxmox_hosts:
|
||||
hosts:
|
||||
proxmox:
|
||||
ansible_host: 192.168.1.10
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
# host.yml — Proxmox Host Playbook
|
||||
|
||||
Configures the Proxmox host (`192.168.1.10`) with:
|
||||
|
||||
- Admin user (`adyrem`) with SSH key auth and passwordless sudo
|
||||
- `claude-code` SFTP-only user, chrooted to `/home/claude-code/workspace`
|
||||
- SSH hardened: key-only auth, root login disabled, port changed to 2222
|
||||
- Unattended security upgrades (Debian security channel only)
|
||||
|
||||
---
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Proxmox is freshly installed and accessible as `root` via SSH on port 22
|
||||
- Ansible is installed on the machine running the playbook
|
||||
- Vault password is at `~/.config/homelab/vault_pass`
|
||||
- Repo is cloned and you're in the `ansible/` directory
|
||||
|
||||
---
|
||||
|
||||
## First run (bootstrap)
|
||||
|
||||
The first run connects as `root` because `adyrem` doesn't exist yet.
|
||||
`group_vars/proxmox_hosts/vars.yml` is already set for this — no changes needed.
|
||||
|
||||
```bash
|
||||
ansible-playbook playbooks/host.yml
|
||||
```
|
||||
|
||||
What happens:
|
||||
1. Creates `adyrem` with your SSH key and passwordless sudo
|
||||
2. Creates `claude-code` user with SFTP-only access
|
||||
3. Changes SSH port to 2222 and disables root login
|
||||
4. Restarts sshd — **root SSH access ends here**
|
||||
|
||||
---
|
||||
|
||||
## After the first run
|
||||
|
||||
Update `inventory/group_vars/proxmox_hosts/vars.yml` to connect as `adyrem`:
|
||||
|
||||
```yaml
|
||||
ansible_user: adyrem
|
||||
ansible_become: true
|
||||
ansible_become_method: sudo
|
||||
ansible_port: 2222
|
||||
```
|
||||
|
||||
Verify you can still connect before relying on this:
|
||||
|
||||
```bash
|
||||
ssh -p 2222 adyrem@192.168.1.10
|
||||
```
|
||||
|
||||
All subsequent runs use:
|
||||
|
||||
```bash
|
||||
ansible-playbook playbooks/host.yml
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## claude-code SFTP access
|
||||
|
||||
The `claude-code` user can only SFTP into `/home/claude-code/workspace`. No shell, no TCP forwarding, no escape from the chroot.
|
||||
|
||||
From the Claude Code VM:
|
||||
|
||||
```bash
|
||||
sftp -P 2222 claude-code@192.168.1.10
|
||||
# lands in /workspace (which is /home/claude-code/workspace on the host)
|
||||
```
|
||||
|
||||
The private key for this user lives in the Claude Code VM at `~/.ssh/claude-code_ed25519`.
|
||||
The corresponding public key is committed at `keys/claude-code_ed25519.pub`.
|
||||
|
||||
---
|
||||
|
||||
## Re-running safely
|
||||
|
||||
The playbook is idempotent. Re-running it after the group_vars update will:
|
||||
- Ensure all config is still correct
|
||||
- Apply any template changes (sshd drop-in, unattended-upgrades)
|
||||
- Restart sshd only if the config changed
|
||||
@@ -0,0 +1,13 @@
|
||||
---
|
||||
# Proxmox host configuration: admin user, claude-code SFTP user, SSH hardening,
|
||||
# unattended security upgrades.
|
||||
#
|
||||
# BOOTSTRAP NOTE: First run connects as root (see group_vars/proxmox_hosts/vars.yml).
|
||||
# After the first run, root SSH login is disabled — update the group_vars to:
|
||||
# ansible_user: adyrem
|
||||
# ansible_become: true
|
||||
# ansible_port: 2222
|
||||
- name: Configure Proxmox host
|
||||
hosts: proxmox_hosts
|
||||
roles:
|
||||
- proxmox_host
|
||||
@@ -0,0 +1,111 @@
|
||||
# Adding a new WireGuard client
|
||||
|
||||
## Current peer assignments
|
||||
|
||||
| Name | IP |
|
||||
|--------|--------------|
|
||||
| server | 10.10.10.1 |
|
||||
| fedora | 10.10.10.2 |
|
||||
|
||||
Pick the next free IP (10.10.10.3, 10.10.10.4, …) for each new machine.
|
||||
|
||||
---
|
||||
|
||||
## Step 1 — Generate a keypair on the new machine
|
||||
|
||||
**Linux / macOS:**
|
||||
```bash
|
||||
wg genkey | tee ~/wg-client.key | wg pubkey
|
||||
```
|
||||
|
||||
**Windows** (WireGuard app installed):
|
||||
Open the WireGuard app → Add Tunnel → Add empty tunnel. It generates a keypair and shows the public key at the top.
|
||||
|
||||
Copy the **public key** — you need it in Step 2.
|
||||
Keep the **private key** local. Never commit it to git.
|
||||
|
||||
---
|
||||
|
||||
## Step 2 — Register the peer in Ansible
|
||||
|
||||
Edit `ansible/inventory/group_vars/proxmox_hosts/vars.yml` and add an entry to `wireguard_peers`:
|
||||
|
||||
```yaml
|
||||
wireguard_peers:
|
||||
- name: fedora
|
||||
public_key: "ivtchk9pxEwYwusMzmn7Uq89LFV3uB1I8iYto0EJuy4="
|
||||
allowed_ips: 10.10.10.2/32
|
||||
- name: my-new-machine # ← add this
|
||||
public_key: "<public key from Step 1>"
|
||||
allowed_ips: 10.10.10.X/32 # ← next free IP
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 3 — Push the peer to the server
|
||||
|
||||
```bash
|
||||
cd ansible && ansible-playbook playbooks/host.yml
|
||||
```
|
||||
|
||||
The server will accept connections from the new client immediately after this.
|
||||
|
||||
---
|
||||
|
||||
## Step 4 — Create the client config
|
||||
|
||||
Use the template below. Store it in a location that is **not** committed to git.
|
||||
|
||||
```ini
|
||||
[Interface]
|
||||
Address = 10.10.10.X/32
|
||||
PrivateKey = <private key from Step 1>
|
||||
DNS = 10.10.1.2
|
||||
|
||||
[Peer]
|
||||
PublicKey = UJaAvoT65+qC7NdD4lKFK+/J1OxKBYp1ZY3ynHjqcHE=
|
||||
Endpoint = adyrem.duckdns.org:51820
|
||||
AllowedIPs = 10.10.10.0/24, 10.10.1.0/24, 10.10.2.0/24
|
||||
PersistentKeepalive = 25
|
||||
```
|
||||
|
||||
`AllowedIPs` is a split tunnel — only homelab traffic goes through the VPN.
|
||||
Replace `adyrem.duckdns.org` with `192.168.1.10` when connecting from inside the LAN.
|
||||
|
||||
---
|
||||
|
||||
## Step 5 — Connect
|
||||
|
||||
**Linux (one-off):**
|
||||
```bash
|
||||
sudo wg-quick up /path/to/wg0.conf
|
||||
# disconnect: sudo wg-quick down /path/to/wg0.conf
|
||||
```
|
||||
|
||||
**Linux (persistent, starts on boot):**
|
||||
```bash
|
||||
sudo cp wg0.conf /etc/wireguard/wg0.conf
|
||||
sudo systemctl enable --now wg-quick@wg0
|
||||
```
|
||||
|
||||
**Windows / macOS / Android / iOS:**
|
||||
Import the config file into the WireGuard app, then toggle the tunnel on.
|
||||
|
||||
---
|
||||
|
||||
## Step 6 — Verify
|
||||
|
||||
```bash
|
||||
ping 10.10.10.1 # Proxmox host
|
||||
ping 10.10.1.137 # monitoring VM
|
||||
# Proxmox web UI: https://10.10.10.1:8006
|
||||
# Grafana: http://10.10.1.137:3000
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Removing a client
|
||||
|
||||
1. Delete the peer entry from `wireguard_peers` in `group_vars/proxmox_hosts/vars.yml`
|
||||
2. Run `ansible-playbook playbooks/host.yml`
|
||||
3. The client's public key is removed from the server — existing sessions drop immediately
|
||||
@@ -1,16 +1,18 @@
|
||||
# Proxmox host — jump host only, no shell
|
||||
Host proxmox
|
||||
HostName 192.168.1.10
|
||||
User root
|
||||
Port 2222
|
||||
User adyrem
|
||||
IdentityFile ~/.ssh/homelab_ed25519
|
||||
StrictHostKeyChecking no
|
||||
StrictHostKeyChecking accept-new
|
||||
|
||||
# Proxmox SFTP — write files to host workspace
|
||||
Host proxmox-sftp
|
||||
HostName 192.168.1.10
|
||||
Port 2222
|
||||
User claude-code
|
||||
IdentityFile ~/.ssh/claude-code_ed25519
|
||||
StrictHostKeyChecking no
|
||||
StrictHostKeyChecking accept-new
|
||||
|
||||
# Infra VMs — via ProxyJump through Proxmox
|
||||
Host gitea-vm
|
||||
@@ -18,25 +20,25 @@ Host gitea-vm
|
||||
User adyrem
|
||||
ProxyJump proxmox
|
||||
IdentityFile ~/.ssh/homelab_ed25519
|
||||
StrictHostKeyChecking no
|
||||
StrictHostKeyChecking accept-new
|
||||
|
||||
Host monitoring-vm
|
||||
HostName 10.10.1.137
|
||||
User adyrem
|
||||
ProxyJump proxmox
|
||||
IdentityFile ~/.ssh/homelab_ed25519
|
||||
StrictHostKeyChecking no
|
||||
StrictHostKeyChecking accept-new
|
||||
|
||||
Host pihole-ct
|
||||
HostName 10.10.1.2
|
||||
User root
|
||||
ProxyJump proxmox
|
||||
IdentityFile ~/.ssh/homelab_ed25519
|
||||
StrictHostKeyChecking no
|
||||
StrictHostKeyChecking accept-new
|
||||
|
||||
Host traefik-ct
|
||||
HostName 10.10.1.3
|
||||
User root
|
||||
ProxyJump proxmox
|
||||
IdentityFile ~/.ssh/homelab_ed25519
|
||||
StrictHostKeyChecking no
|
||||
StrictHostKeyChecking accept-new
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
---
|
||||
gitea_http_port: 3000
|
||||
gitea_domain: "{{ ansible_host }}"
|
||||
gitea_root_url: "https://adyrem.duckdns.org/git/"
|
||||
gitea_db_name: gitea
|
||||
gitea_db_user: gitea
|
||||
gitea_db_password: "{{ vault_gitea_db_password }}"
|
||||
|
||||
@@ -6,7 +6,7 @@ RUN_MODE = prod
|
||||
DOMAIN = {{ gitea_domain }}
|
||||
HTTP_ADDR = 0.0.0.0
|
||||
HTTP_PORT = {{ gitea_http_port }}
|
||||
ROOT_URL = http://{{ gitea_domain }}:{{ gitea_http_port }}/
|
||||
ROOT_URL = {{ gitea_root_url }}
|
||||
SSH_DOMAIN = {{ gitea_domain }}
|
||||
SSH_PORT = 22
|
||||
START_SSH_SERVER = false
|
||||
|
||||
@@ -9,3 +9,4 @@ pihole_dns_hosts:
|
||||
- { ip: "10.10.1.3", host: "traefik.homelab" }
|
||||
- { ip: "10.10.1.125", host: "gitea.homelab" }
|
||||
- { ip: "10.10.1.137", host: "grafana.homelab" }
|
||||
- { ip: "10.10.1.3", host: "adyrem.duckdns.org" }
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
ssh_port: 2222
|
||||
@@ -0,0 +1,8 @@
|
||||
[Unit]
|
||||
Description=DuckDNS IP update
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/usr/local/bin/duckdns-update.sh
|
||||
@@ -0,0 +1,9 @@
|
||||
[Unit]
|
||||
Description=DuckDNS IP update — every 5 minutes
|
||||
|
||||
[Timer]
|
||||
OnBootSec=2min
|
||||
OnUnitActiveSec=5min
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
@@ -0,0 +1,49 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Ensure Proxmox host firewall rules exist via pvesh. Idempotent."""
|
||||
import json
|
||||
import subprocess
|
||||
|
||||
NODE = subprocess.run(['hostname'], capture_output=True, text=True).stdout.strip()
|
||||
|
||||
DESIRED = [
|
||||
{'proto': 'tcp', 'source': '192.168.1.0/24', 'dport': '2222'},
|
||||
{'proto': 'tcp', 'source': '10.10.10.0/24', 'dport': '2222'},
|
||||
{'proto': 'tcp', 'source': '192.168.1.0/24', 'dport': '8006'},
|
||||
{'proto': 'tcp', 'source': '10.10.10.0/24', 'dport': '8006'},
|
||||
{'proto': 'tcp', 'source': '192.168.1.0/24', 'dport': '3128'},
|
||||
{'proto': 'udp', 'source': '10.10.0.0/16', 'dport': '53'},
|
||||
{'proto': 'tcp', 'source': '10.10.0.0/16', 'dport': '53'},
|
||||
{'proto': 'udp', 'dport': '51820'},
|
||||
{'proto': 'icmp'},
|
||||
]
|
||||
|
||||
|
||||
def pvesh(*args):
|
||||
return subprocess.run(['pvesh', *args], capture_output=True, text=True)
|
||||
|
||||
|
||||
def get_rules():
|
||||
r = pvesh('get', f'/nodes/{NODE}/firewall/rules', '--output-format', 'json')
|
||||
try:
|
||||
return json.loads(r.stdout)
|
||||
except json.JSONDecodeError:
|
||||
return []
|
||||
|
||||
|
||||
def matches(current, desired):
|
||||
return all(str(current.get(k, '')) == str(v) for k, v in desired.items())
|
||||
|
||||
|
||||
endpoint = f'/nodes/{NODE}/firewall/rules'
|
||||
current = get_rules()
|
||||
changed = False
|
||||
|
||||
for rule in DESIRED:
|
||||
if not any(matches(c, rule) for c in current):
|
||||
args = ['create', endpoint, '--action', 'ACCEPT', '--type', 'in', '--enable', '1']
|
||||
for k, v in rule.items():
|
||||
args += [f'--{k}', str(v)]
|
||||
pvesh(*args)
|
||||
changed = True
|
||||
|
||||
print('changed' if changed else 'ok')
|
||||
@@ -0,0 +1,14 @@
|
||||
- name: Restart sshd
|
||||
ansible.builtin.service:
|
||||
name: ssh
|
||||
state: restarted
|
||||
|
||||
- name: Restart WireGuard
|
||||
ansible.builtin.systemd:
|
||||
name: wg-quick@wg0
|
||||
state: restarted
|
||||
|
||||
- name: Reload Proxmox firewall
|
||||
ansible.builtin.service:
|
||||
name: pve-firewall
|
||||
state: restarted
|
||||
@@ -0,0 +1,207 @@
|
||||
# --- Proxmox firewall ---
|
||||
|
||||
- name: Ensure Proxmox datacenter firewall is enabled
|
||||
ansible.builtin.shell: pvesh set /cluster/firewall/options --enable 1
|
||||
changed_when: false
|
||||
|
||||
- name: Ensure host firewall rules
|
||||
ansible.builtin.script:
|
||||
cmd: "{{ role_path }}/files/pve_firewall.py"
|
||||
executable: /usr/bin/python3
|
||||
register: _pve_fw
|
||||
changed_when: "'changed' in _pve_fw.stdout"
|
||||
failed_when: false
|
||||
|
||||
# --- Packages ---
|
||||
|
||||
- name: Install required packages
|
||||
ansible.builtin.apt:
|
||||
name:
|
||||
- sudo
|
||||
- unattended-upgrades
|
||||
- apt-listchanges
|
||||
- wireguard
|
||||
state: present
|
||||
update_cache: true
|
||||
|
||||
# --- Admin user ---
|
||||
|
||||
- name: Ensure admin user exists
|
||||
ansible.builtin.user:
|
||||
name: "{{ admin_user }}"
|
||||
shell: /bin/bash
|
||||
groups: sudo
|
||||
append: true
|
||||
create_home: true
|
||||
|
||||
- name: Authorize admin SSH key
|
||||
ansible.posix.authorized_key:
|
||||
user: "{{ admin_user }}"
|
||||
key: "{{ admin_ssh_key }}"
|
||||
state: present
|
||||
exclusive: true
|
||||
|
||||
- name: Grant admin user passwordless sudo
|
||||
ansible.builtin.copy:
|
||||
dest: /etc/sudoers.d/{{ admin_user }}
|
||||
content: "{{ admin_user }} ALL=(ALL:ALL) NOPASSWD: ALL\n"
|
||||
owner: root
|
||||
group: root
|
||||
mode: '0440'
|
||||
validate: visudo -cf %s
|
||||
|
||||
# --- claude-code SFTP user ---
|
||||
|
||||
- name: Ensure claude-code user exists
|
||||
ansible.builtin.user:
|
||||
name: claude-code
|
||||
shell: /usr/sbin/nologin
|
||||
home: /home/claude-code
|
||||
create_home: false
|
||||
system: false
|
||||
|
||||
- name: Chroot base directory — root-owned (OpenSSH requirement)
|
||||
ansible.builtin.file:
|
||||
path: /home/claude-code
|
||||
state: directory
|
||||
owner: root
|
||||
group: root
|
||||
mode: '0755'
|
||||
|
||||
- name: .ssh directory for claude-code authorized_keys
|
||||
ansible.builtin.file:
|
||||
path: /home/claude-code/.ssh
|
||||
state: directory
|
||||
owner: root
|
||||
group: root
|
||||
mode: '0755'
|
||||
|
||||
- name: Authorize claude-code SSH key
|
||||
ansible.builtin.copy:
|
||||
dest: /home/claude-code/.ssh/authorized_keys
|
||||
content: "{{ claude_code_ssh_key }}\n"
|
||||
owner: root
|
||||
group: root
|
||||
mode: '0644'
|
||||
|
||||
- name: Writable workspace inside chroot
|
||||
ansible.builtin.file:
|
||||
path: /home/claude-code/workspace
|
||||
state: directory
|
||||
owner: claude-code
|
||||
group: claude-code
|
||||
mode: '0755'
|
||||
|
||||
# --- SSH hardening ---
|
||||
|
||||
# Change port in the main config so sshd listens only on ssh_port.
|
||||
# A drop-in Port directive would add a second port, not replace the default.
|
||||
- name: Set SSH port
|
||||
ansible.builtin.lineinfile:
|
||||
path: /etc/ssh/sshd_config
|
||||
regexp: '^#?Port\s'
|
||||
line: "Port {{ ssh_port }}"
|
||||
notify: Restart sshd
|
||||
|
||||
- name: Deploy SSH hardening drop-in
|
||||
ansible.builtin.template:
|
||||
src: sshd_homelab.conf.j2
|
||||
dest: /etc/ssh/sshd_config.d/99-homelab.conf
|
||||
owner: root
|
||||
group: root
|
||||
mode: '0600'
|
||||
notify: Restart sshd
|
||||
|
||||
# --- Unattended upgrades ---
|
||||
|
||||
- name: Deploy unattended-upgrades config
|
||||
ansible.builtin.template:
|
||||
src: 50unattended-upgrades.j2
|
||||
dest: /etc/apt/apt.conf.d/50unattended-upgrades
|
||||
owner: root
|
||||
group: root
|
||||
mode: '0644'
|
||||
|
||||
- name: Enable daily security updates
|
||||
ansible.builtin.copy:
|
||||
dest: /etc/apt/apt.conf.d/20auto-upgrades
|
||||
content: |
|
||||
APT::Periodic::Update-Package-Lists "1";
|
||||
APT::Periodic::Unattended-Upgrade "1";
|
||||
owner: root
|
||||
group: root
|
||||
mode: '0644'
|
||||
|
||||
# --- DuckDNS dynamic DNS updater ---
|
||||
|
||||
- name: Deploy DuckDNS update script
|
||||
ansible.builtin.template:
|
||||
src: duckdns-update.sh.j2
|
||||
dest: /usr/local/bin/duckdns-update.sh
|
||||
owner: root
|
||||
group: root
|
||||
mode: '0700'
|
||||
|
||||
- name: Install DuckDNS systemd service
|
||||
ansible.builtin.copy:
|
||||
src: duckdns.service
|
||||
dest: /etc/systemd/system/duckdns.service
|
||||
owner: root
|
||||
group: root
|
||||
mode: '0644'
|
||||
|
||||
- name: Install DuckDNS systemd timer
|
||||
ansible.builtin.copy:
|
||||
src: duckdns.timer
|
||||
dest: /etc/systemd/system/duckdns.timer
|
||||
owner: root
|
||||
group: root
|
||||
mode: '0644'
|
||||
|
||||
- name: Enable and start DuckDNS timer
|
||||
ansible.builtin.systemd:
|
||||
name: duckdns.timer
|
||||
enabled: true
|
||||
state: started
|
||||
daemon_reload: true
|
||||
|
||||
# --- WireGuard VPN ---
|
||||
|
||||
- name: Enable IP forwarding
|
||||
ansible.posix.sysctl:
|
||||
name: net.ipv4.ip_forward
|
||||
value: '1'
|
||||
sysctl_set: true
|
||||
reload: true
|
||||
|
||||
- name: Generate WireGuard server keypair
|
||||
ansible.builtin.shell: |
|
||||
wg genkey > /etc/wireguard/server.key
|
||||
chmod 600 /etc/wireguard/server.key
|
||||
wg pubkey < /etc/wireguard/server.key > /etc/wireguard/server.pub
|
||||
args:
|
||||
creates: /etc/wireguard/server.key
|
||||
|
||||
- name: Read server private key
|
||||
ansible.builtin.slurp:
|
||||
src: /etc/wireguard/server.key
|
||||
register: _wg_server_key
|
||||
no_log: true
|
||||
|
||||
- name: Deploy wg0.conf
|
||||
ansible.builtin.template:
|
||||
src: wg0.conf.j2
|
||||
dest: /etc/wireguard/wg0.conf
|
||||
owner: root
|
||||
group: root
|
||||
mode: '0600'
|
||||
vars:
|
||||
wg_server_private_key: "{{ _wg_server_key.content | b64decode | trim }}"
|
||||
notify: Restart WireGuard
|
||||
|
||||
- name: Enable and start WireGuard
|
||||
ansible.builtin.systemd:
|
||||
name: wg-quick@wg0
|
||||
enabled: true
|
||||
state: started
|
||||
daemon_reload: true
|
||||
@@ -0,0 +1,13 @@
|
||||
// Managed by Ansible — security patches only
|
||||
Unattended-Upgrade::Origins-Pattern {
|
||||
"origin=Debian,codename=${distro_codename}-security,label=Debian-Security";
|
||||
"origin=Debian,codename=${distro_codename},label=Debian-Security";
|
||||
};
|
||||
|
||||
Unattended-Upgrade::Package-Blacklist {
|
||||
};
|
||||
|
||||
Unattended-Upgrade::AutoFixInterruptedDpkg "true";
|
||||
Unattended-Upgrade::MinimalSteps "true";
|
||||
Unattended-Upgrade::Remove-Unused-Dependencies "false";
|
||||
Unattended-Upgrade::Automatic-Reboot "false";
|
||||
@@ -0,0 +1,4 @@
|
||||
#!/usr/bin/env bash
|
||||
result=$(curl -s "https://www.duckdns.org/update?domains={{ duckdns_domain }}&token={{ duckdns_token }}&ip=")
|
||||
echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) $result" >> /var/log/duckdns.log
|
||||
[[ "$result" == "OK" ]] || exit 1
|
||||
@@ -0,0 +1,11 @@
|
||||
# Managed by Ansible — do not edit manually
|
||||
PasswordAuthentication no
|
||||
PermitRootLogin no
|
||||
PubkeyAuthentication yes
|
||||
AuthenticationMethods publickey
|
||||
|
||||
Match User claude-code
|
||||
ForceCommand internal-sftp
|
||||
ChrootDirectory /home/claude-code
|
||||
AllowTcpForwarding no
|
||||
X11Forwarding no
|
||||
@@ -0,0 +1,15 @@
|
||||
# Managed by Ansible — do not edit manually
|
||||
[Interface]
|
||||
Address = 10.10.10.1/24
|
||||
ListenPort = {{ wireguard_port }}
|
||||
PrivateKey = {{ wg_server_private_key }}
|
||||
PostUp = iptables -I FORWARD -i %i -j ACCEPT; iptables -I FORWARD -o %i -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
|
||||
PostDown = iptables -D FORWARD -i %i -j ACCEPT; iptables -D FORWARD -o %i -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
|
||||
|
||||
{% for peer in wireguard_peers %}
|
||||
[Peer]
|
||||
# {{ peer.name }}
|
||||
PublicKey = {{ peer.public_key }}
|
||||
AllowedIPs = {{ peer.allowed_ips }}
|
||||
|
||||
{% endfor %}
|
||||
@@ -4,11 +4,15 @@ traefik_http_port: 80
|
||||
traefik_https_port: 443
|
||||
traefik_dashboard_port: 8080
|
||||
traefik_log_level: "INFO"
|
||||
traefik_acme_email: "fanfohcd@gmail.com"
|
||||
|
||||
traefik_services:
|
||||
- name: gitea
|
||||
host: "gitea.homelab"
|
||||
backend: "http://10.10.1.125:3000"
|
||||
public_host: "adyrem.duckdns.org"
|
||||
public_path: "/git"
|
||||
strip_prefix: "/git"
|
||||
- name: grafana
|
||||
host: "grafana.homelab"
|
||||
backend: "http://10.10.1.137:3000"
|
||||
|
||||
@@ -26,6 +26,15 @@
|
||||
- /etc/traefik
|
||||
- /etc/traefik/conf.d
|
||||
|
||||
- name: Create acme.json for Let's Encrypt certificate storage
|
||||
ansible.builtin.copy:
|
||||
dest: /etc/traefik/acme.json
|
||||
content: ""
|
||||
owner: traefik
|
||||
group: traefik
|
||||
mode: "0600"
|
||||
force: false
|
||||
|
||||
- name: Check installed Traefik version
|
||||
ansible.builtin.command:
|
||||
cmd: /usr/local/bin/traefik version
|
||||
|
||||
@@ -1,11 +1,45 @@
|
||||
http:
|
||||
routers:
|
||||
{% for svc in traefik_services %}
|
||||
{{ svc.name }}:
|
||||
{{ svc.name }}-internal:
|
||||
rule: "Host(`{{ svc.host }}`)"
|
||||
service: {{ svc.name }}
|
||||
entryPoints:
|
||||
- web
|
||||
{% if svc.public_host is defined %}
|
||||
{{ svc.name }}-public-redirect:
|
||||
rule: "Host(`{{ svc.public_host }}`){% if svc.public_path is defined %} && PathPrefix(`{{ svc.public_path }}`){% endif %}"
|
||||
service: {{ svc.name }}
|
||||
entryPoints:
|
||||
- web
|
||||
middlewares:
|
||||
- redirect-https
|
||||
{{ svc.name }}-public:
|
||||
rule: "Host(`{{ svc.public_host }}`){% if svc.public_path is defined %} && PathPrefix(`{{ svc.public_path }}`){% endif %}"
|
||||
service: {{ svc.name }}
|
||||
entryPoints:
|
||||
- websecure
|
||||
tls:
|
||||
certResolver: letsencrypt
|
||||
{% if svc.strip_prefix is defined %}
|
||||
middlewares:
|
||||
- {{ svc.name }}-strip-prefix
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
middlewares:
|
||||
redirect-https:
|
||||
redirectScheme:
|
||||
scheme: https
|
||||
permanent: true
|
||||
{% for svc in traefik_services %}
|
||||
{% if svc.strip_prefix is defined %}
|
||||
{{ svc.name }}-strip-prefix:
|
||||
stripPrefix:
|
||||
prefixes:
|
||||
- "{{ svc.strip_prefix }}"
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
services:
|
||||
|
||||
@@ -5,11 +5,21 @@ api:
|
||||
entryPoints:
|
||||
web:
|
||||
address: ":{{ traefik_http_port }}"
|
||||
websecure:
|
||||
address: ":{{ traefik_https_port }}"
|
||||
|
||||
providers:
|
||||
file:
|
||||
directory: /etc/traefik/conf.d
|
||||
watch: true
|
||||
|
||||
certificatesResolvers:
|
||||
letsencrypt:
|
||||
acme:
|
||||
email: {{ traefik_acme_email }}
|
||||
storage: /etc/traefik/acme.json
|
||||
httpChallenge:
|
||||
entryPoint: web
|
||||
|
||||
log:
|
||||
level: {{ traefik_log_level }}
|
||||
|
||||
@@ -4,9 +4,12 @@ log_level_in: nolog
|
||||
log_level_out: nolog
|
||||
|
||||
[RULES]
|
||||
IN ACCEPT -source 192.168.1.0/24 -p tcp -dport 22
|
||||
IN ACCEPT -source 192.168.1.0/24 -p tcp -dport 2222
|
||||
IN ACCEPT -source 10.10.10.0/24 -p tcp -dport 2222
|
||||
IN ACCEPT -source 192.168.1.0/24 -p tcp -dport 8006
|
||||
IN ACCEPT -source 10.10.10.0/24 -p tcp -dport 8006
|
||||
IN ACCEPT -source 192.168.1.0/24 -p tcp -dport 3128
|
||||
IN ACCEPT -source 10.10.0.0/16 -p udp -dport 53
|
||||
IN ACCEPT -source 10.10.0.0/16 -p tcp -dport 53
|
||||
IN ACCEPT -p udp -dport 51820
|
||||
IN ACCEPT -p icmp
|
||||
|
||||
Reference in New Issue
Block a user