server_init/playbook.yml
Nelis 397d4fcc48 Fix Forgejo SSH passthrough: use -u git and correct app.ini path
docker exec needs -u git to avoid running as root (Forgejo refuses root),
and the config path inside the container is /data/gitea/conf/app.ini.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-28 10:51:33 +00:00

405 lines
11 KiB
YAML

---
- name: Server Initialization Playbook
hosts: localhost
connection: local
become: yes
vars:
base_domain:
pangolin_domain: "pangolin.{{ base_domain }}"
forgejo_domain: "forgejo.{{ base_domain }}"
opencode_domain: "opencode.{{ base_domain }}"
acme_email: "admin@{{ base_domain }}"
docker_hosting_dir: "/home/{{ login_user }}/docker_hosting"
resources_dir: "{{ playbook_dir }}/resources"
login_user: "{{ ansible_env.SUDO_USER | default(ansible_user_id) }}"
deb_architecture: "{{ 'amd64' if ansible_architecture == 'x86_64' else 'arm64' if ansible_architecture == 'aarch64' else ansible_architecture }}"
tasks:
- name: Update apt cache
ansible.builtin.apt:
update_cache: yes
cache_valid_time: 3600
- name: Install prerequisites for Docker repository
ansible.builtin.apt:
name:
- ca-certificates
- curl
- gnupg
- lsb-release
state: present
- name: Install common dev dependencies
ansible.builtin.apt:
name:
- git
- vim
- wget
- htop
- tmux
- unzip
- zip
- build-essential
- gcc
- make
- pkg-config
- libssl-dev
- libffi-dev
- python3
- python3-pip
- python3-venv
- nodejs
- npm
- jq
state: present
# --- Server hardening ---
- name: Install security packages
ansible.builtin.apt:
name:
- ufw
- fail2ban
- unattended-upgrades
- apt-listchanges
state: present
- name: Disable root SSH login
ansible.builtin.lineinfile:
path: /etc/ssh/sshd_config
regexp: '^#?PermitRootLogin'
line: 'PermitRootLogin no'
validate: 'sshd -t -f %s'
notify: restart sshd
- name: Disable SSH password authentication
ansible.builtin.lineinfile:
path: /etc/ssh/sshd_config
regexp: '^#?PasswordAuthentication'
line: 'PasswordAuthentication no'
validate: 'sshd -t -f %s'
notify: restart sshd
- name: Disable SSH challenge-response authentication
ansible.builtin.lineinfile:
path: /etc/ssh/sshd_config
regexp: '^#?KbdInteractiveAuthentication'
line: 'KbdInteractiveAuthentication no'
validate: 'sshd -t -f %s'
notify: restart sshd
- name: Set SSH MaxAuthTries
ansible.builtin.lineinfile:
path: /etc/ssh/sshd_config
regexp: '^#?MaxAuthTries'
line: 'MaxAuthTries 3'
validate: 'sshd -t -f %s'
notify: restart sshd
- name: UFW - Allow forwarding for Docker
ansible.builtin.lineinfile:
path: /etc/default/ufw
regexp: '^DEFAULT_FORWARD_POLICY='
line: 'DEFAULT_FORWARD_POLICY="ACCEPT"'
- name: UFW - Set default deny incoming
community.general.ufw:
direction: incoming
default: deny
- name: UFW - Set default allow outgoing
community.general.ufw:
direction: outgoing
default: allow
- name: UFW - Allow SSH
community.general.ufw:
rule: allow
port: '22'
proto: tcp
- name: UFW - Allow HTTP
community.general.ufw:
rule: allow
port: '80'
proto: tcp
- name: UFW - Allow HTTPS
community.general.ufw:
rule: allow
port: '443'
proto: tcp
- name: UFW - Allow WireGuard
community.general.ufw:
rule: allow
port: '51820'
proto: udp
- name: UFW - Allow WireGuard clients
community.general.ufw:
rule: allow
port: '21820'
proto: udp
- name: UFW - Allow Docker to reach opencode on host
community.general.ufw:
rule: allow
from_ip: 172.18.0.0/16
to_ip: any
port: '3000'
proto: tcp
- name: UFW - Enable firewall
community.general.ufw:
state: enabled
- name: Configure fail2ban for SSH
ansible.builtin.copy:
dest: /etc/fail2ban/jail.local
mode: '0644'
content: |
[DEFAULT]
bantime = 1h
findtime = 10m
maxretry = 5
[sshd]
enabled = true
port = ssh
filter = sshd
logpath = /var/log/auth.log
- name: Start and enable fail2ban
ansible.builtin.service:
name: fail2ban
state: started
enabled: yes
- name: Enable unattended security upgrades
ansible.builtin.copy:
dest: /etc/apt/apt.conf.d/20auto-upgrades
mode: '0644'
content: |
APT::Periodic::Update-Package-Lists "1";
APT::Periodic::Unattended-Upgrade "1";
APT::Periodic::AutocleanInterval "7";
- name: Create keyrings directory
ansible.builtin.file:
path: /etc/apt/keyrings
state: directory
mode: '0755'
- name: Add Docker GPG key
ansible.builtin.shell:
cmd: curl -fsSL https://download.docker.com/linux/{{ ansible_distribution | lower }}/gpg -o /etc/apt/keyrings/docker.asc
args:
creates: /etc/apt/keyrings/docker.asc
- name: Add Docker repository
ansible.builtin.apt_repository:
repo: "deb [arch={{ deb_architecture }} signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/{{ ansible_distribution | lower }} {{ ansible_distribution_release }} stable"
state: present
- name: Install Docker
ansible.builtin.apt:
name:
- docker-ce
- docker-ce-cli
- containerd.io
- docker-compose-plugin
state: present
update_cache: yes
- name: Start and enable Docker
ansible.builtin.service:
name: docker
state: started
enabled: yes
- name: Add user to docker group
ansible.builtin.user:
name: "{{ login_user }}"
groups: docker
append: yes
- name: Install Rust via rustup for login user
ansible.builtin.shell:
cmd: curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
args:
creates: "/home/{{ login_user }}/.cargo/bin/rustup"
become_user: "{{ login_user }}"
- name: Install uv (Python package manager)
ansible.builtin.shell:
cmd: curl -LsSf https://astral.sh/uv/install.sh | sh
args:
creates: "/home/{{ login_user }}/.local/bin/uv"
become_user: "{{ login_user }}"
# --- Forgejo SSH passthrough ---
- name: Create forgejo-shell directory
ansible.builtin.file:
path: /etc/forgejo/bin
state: directory
mode: '0755'
- name: Create forgejo-shell script
ansible.builtin.copy:
dest: /etc/forgejo/bin/forgejo-shell
mode: '0755'
content: |
#!/bin/sh
/usr/bin/docker exec -i -u git --env SSH_ORIGINAL_COMMAND="$SSH_ORIGINAL_COMMAND" forgejo sh "$@"
- name: Create git user for Forgejo
ansible.builtin.user:
name: git
system: yes
shell: /etc/forgejo/bin/forgejo-shell
home: /home/git
create_home: yes
register: git_user
- name: Set git user UID/GID facts
ansible.builtin.set_fact:
git_uid: "{{ git_user.uid }}"
git_gid: "{{ git_user.group }}"
- name: Add git user to docker group
ansible.builtin.user:
name: git
groups: docker
append: yes
- name: Configure SSH passthrough for git user
ansible.builtin.copy:
dest: /etc/ssh/sshd_config.d/forgejo-ssh-passthrough.conf
mode: '0644'
content: |
Match User git
AuthorizedKeysCommandUser git
AuthorizedKeysCommand /usr/bin/docker exec -i -u git forgejo /usr/local/bin/gitea keys -c /data/gitea/conf/app.ini -e git -u %u -t %t -k %k
notify: restart sshd
- name: Generate Pangolin secret
ansible.builtin.shell:
cmd: openssl rand -base64 48
register: pangolin_secret_result
changed_when: false
- name: Set Pangolin secret fact
ansible.builtin.set_fact:
pangolin_secret: "{{ pangolin_secret_result.stdout }}"
# --- Directory structure ---
- name: Create docker_hosting directory structure
ansible.builtin.file:
path: "{{ docker_hosting_dir }}/{{ item }}"
state: directory
owner: "{{ login_user }}"
group: "{{ login_user }}"
mode: '0755'
loop:
- docker_data/pangolin
- docker_data/pangolin/db
- docker_data/pangolin/letsencrypt
- docker_data/pangolin/logs
- docker_data/pangolin/traefik
- docker_data/pangolin/traefik/logs
- name: Create Forgejo data directory
ansible.builtin.file:
path: "{{ docker_hosting_dir }}/docker_data/forgejo/data"
state: directory
owner: git
group: git
mode: '0755'
# --- Pangolin config files ---
- name: Copy Pangolin config.yml
ansible.builtin.template:
src: "{{ resources_dir }}/pangolin/config.yml.j2"
dest: "{{ docker_hosting_dir }}/docker_data/pangolin/config.yml"
owner: "{{ login_user }}"
group: "{{ login_user }}"
mode: '0600'
force: no
- name: Copy Traefik static config
ansible.builtin.template:
src: "{{ resources_dir }}/pangolin/traefik_config.yml.j2"
dest: "{{ docker_hosting_dir }}/docker_data/pangolin/traefik/traefik_config.yml"
owner: "{{ login_user }}"
group: "{{ login_user }}"
mode: '0644'
- name: Copy Traefik dynamic config
ansible.builtin.template:
src: "{{ resources_dir }}/pangolin/dynamic_config.yml.j2"
dest: "{{ docker_hosting_dir }}/docker_data/pangolin/traefik/dynamic_config.yml"
owner: "{{ login_user }}"
group: "{{ login_user }}"
mode: '0644'
- name: Copy compose.yml
ansible.builtin.template:
src: "{{ resources_dir }}/compose.yml.j2"
dest: "{{ docker_hosting_dir }}/compose.yml"
owner: "{{ login_user }}"
group: "{{ login_user }}"
mode: '0644'
# --- OpenCode ---
- name: Install opencode
ansible.builtin.shell:
cmd: curl -fsSL https://opencode.ai/install | bash
args:
creates: /home/{{ login_user }}/.opencode/bin/opencode
become_user: "{{ login_user }}"
- name: Create opencode systemd service
ansible.builtin.copy:
dest: /etc/systemd/system/opencode.service
mode: '0644'
content: |
[Unit]
Description=OpenCode Server
After=network.target
[Service]
Type=simple
User={{ login_user }}
WorkingDirectory=/home/{{ login_user }}
ExecStart=/home/{{ login_user }}/.opencode/bin/opencode serve --port 3000 --hostname 0.0.0.0
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
- name: Start and enable opencode service
ansible.builtin.systemd:
name: opencode
state: started
enabled: yes
daemon_reload: yes
# --- Start services ---
- name: Start all services via docker compose
ansible.builtin.shell:
cmd: docker compose up -d
chdir: "{{ docker_hosting_dir }}"
handlers:
- name: restart sshd
ansible.builtin.service:
name: ssh
state: restarted