Add Hetzner VPS to Tailscale tailnet for private deployment access #141

Open
opened 2026-03-28 10:31:17 +01:00 by marcel · 0 comments
Owner

Why

The Gitea instance and act-runner live on the home LAN — not reachable from the public internet. The Hetzner VPS is on the public internet. For CI to deploy to the VPS, the two need a secure private channel.

Tailscale is already used on the home network. Adding the VPS to the same tailnet gives the act-runner a stable, authenticated route to the VPS over WireGuard — no open firewall ports, no VPN server to maintain, no dynamic IP problems.

This is a prerequisite for the CI deploy pipeline (#142 and #143).

What to do

1. Install Tailscale on the VPS

curl -fsSL https://tailscale.com/install.sh | sh

2. Authenticate and join the tailnet

Generate a reusable auth key in the Tailscale admin console (tailscale.com/admin/settings/keys):

  • Set it as reusable if you may need to re-register
  • Tag it (e.g. tag:vps) so it gets the right ACL policy
tailscale up --authkey=<your-auth-key> --hostname=familienarchiv-vps

3. Verify connectivity from the act-runner host

From the home server where the act-runner runs:

tailscale ping familienarchiv-vps
# Should show: pong from familienarchiv-vps (100.x.x.x)

Note the assigned Tailscale IP (tailscale ip -4 on the VPS).

4. Lock down the VPS firewall

The VPS should only accept SSH over Tailscale, not from the public internet. Only ports 80 and 443 (Caddy) need to be publicly open.

# Using ufw
ufw default deny incoming
ufw allow 80/tcp    # HTTP (Caddy, for Let's Encrypt challenge)
ufw allow 443/tcp   # HTTPS
ufw allow 443/udp   # HTTP/3
ufw allow in on tailscale0   # allow all traffic over Tailscale interface
ufw enable

This means ssh user@<public-ip> stops working — SSH only works via ssh user@<tailscale-ip>. That is intentional.

5. Create a deploy user on the VPS

Avoid using root or your personal user for CI deploys. Create a dedicated deploy user with Docker access:

useradd -m -s /bin/bash deploy
usermod -aG docker deploy
mkdir -p /home/deploy/.ssh
chmod 700 /home/deploy/.ssh

The deploy user's authorized_keys will be populated in #142 (SSH key setup).

6. Store the Tailscale IP as a Gitea secret

In Gitea → repo settings → Secrets:

  • Name: VPS_TAILSCALE_IP
  • Value: the Tailscale IP of the VPS (e.g. 100.64.x.x)

This secret is used by the deploy job in #142.

Acceptance criteria

  • tailscale ping familienarchiv-vps succeeds from the home server.
  • ssh deploy@<VPS_TAILSCALE_IP> works from the home server.
  • ssh deploy@<VPS_PUBLIC_IP> is refused (firewall blocks public SSH).
  • Ports 80 and 443 are reachable from the public internet (needed for Caddy TLS).
  • VPS_TAILSCALE_IP secret exists in Gitea repo settings.
## Why The Gitea instance and act-runner live on the home LAN — not reachable from the public internet. The Hetzner VPS is on the public internet. For CI to deploy to the VPS, the two need a secure private channel. Tailscale is already used on the home network. Adding the VPS to the same tailnet gives the act-runner a stable, authenticated route to the VPS over WireGuard — no open firewall ports, no VPN server to maintain, no dynamic IP problems. This is a prerequisite for the CI deploy pipeline (#142 and #143). ## What to do ### 1. Install Tailscale on the VPS ```bash curl -fsSL https://tailscale.com/install.sh | sh ``` ### 2. Authenticate and join the tailnet Generate a reusable auth key in the Tailscale admin console (tailscale.com/admin/settings/keys): - Set it as reusable if you may need to re-register - Tag it (e.g. `tag:vps`) so it gets the right ACL policy ```bash tailscale up --authkey=<your-auth-key> --hostname=familienarchiv-vps ``` ### 3. Verify connectivity from the act-runner host From the home server where the act-runner runs: ```bash tailscale ping familienarchiv-vps # Should show: pong from familienarchiv-vps (100.x.x.x) ``` Note the assigned Tailscale IP (`tailscale ip -4` on the VPS). ### 4. Lock down the VPS firewall The VPS should only accept SSH over Tailscale, not from the public internet. Only ports 80 and 443 (Caddy) need to be publicly open. ```bash # Using ufw ufw default deny incoming ufw allow 80/tcp # HTTP (Caddy, for Let's Encrypt challenge) ufw allow 443/tcp # HTTPS ufw allow 443/udp # HTTP/3 ufw allow in on tailscale0 # allow all traffic over Tailscale interface ufw enable ``` This means `ssh user@<public-ip>` stops working — SSH only works via `ssh user@<tailscale-ip>`. That is intentional. ### 5. Create a deploy user on the VPS Avoid using root or your personal user for CI deploys. Create a dedicated `deploy` user with Docker access: ```bash useradd -m -s /bin/bash deploy usermod -aG docker deploy mkdir -p /home/deploy/.ssh chmod 700 /home/deploy/.ssh ``` The deploy user's `authorized_keys` will be populated in #142 (SSH key setup). ### 6. Store the Tailscale IP as a Gitea secret In Gitea → repo settings → Secrets: - **Name:** `VPS_TAILSCALE_IP` - **Value:** the Tailscale IP of the VPS (e.g. `100.64.x.x`) This secret is used by the deploy job in #142. ## Acceptance criteria - `tailscale ping familienarchiv-vps` succeeds from the home server. - `ssh deploy@<VPS_TAILSCALE_IP>` works from the home server. - `ssh deploy@<VPS_PUBLIC_IP>` is refused (firewall blocks public SSH). - Ports 80 and 443 are reachable from the public internet (needed for Caddy TLS). - `VPS_TAILSCALE_IP` secret exists in Gitea repo settings.
marcel added the devopsphase-3: prod-compose labels 2026-03-28 10:46:49 +01:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: marcel/familienarchiv#141