# Cloudflare Tunnel Setup for Incus Host

## Overview

Single routing point for all services across Incus instances.
Services self-declare by dropping a file into a shared directory.
No port forwarding, no OPNsense reverse proxy config, no DDNS.

## Prerequisites

- Domain: ampbox.io (registrar: Namecheap)
- Cloudflare account (free tier) with ampbox.io DNS delegated
  - At Namecheap: set nameservers to the ones Cloudflare assigns
  - Verify domain is active in Cloudflare dashboard

## 1. Install cloudflared on the Incus host

```bash
curl -fsSL https://pkg.cloudflare.com/cloudflare-main.gpg \
  | sudo tee /usr/share/keyrings/cloudflare-main.gpg >/dev/null
echo "deb [signed-by=/usr/share/keyrings/cloudflare-main.gpg] \
  https://pkg.cloudflare.com/cloudflared $(lsb_release -cs) main" \
  | sudo tee /etc/apt/sources.list.d/cloudflared.list
sudo apt update && sudo apt install -y cloudflared
```

## 2. Authenticate and create the tunnel

```bash
cloudflared tunnel login          # opens browser, authorize ampbox.io
cloudflared tunnel create ampbox  # creates tunnel, saves credentials JSON
cloudflared tunnel route dns ampbox "*.ampbox.io"  # wildcard DNS route
```

Note the credentials file path printed (typically
`~/.cloudflared/<TUNNEL_ID>.json`). You need it for step 4.

## 3. Create the service declaration directory

```bash
sudo mkdir -p /etc/cloudflared/services.d
```

## 4. Write the main tunnel config

```bash
# Replace <TUNNEL_ID> with your actual tunnel ID from step 2
sudo tee /etc/cloudflared/config.yml << 'EOF'
tunnel: ampbox
credentials-file: /root/.cloudflared/<TUNNEL_ID>.json

ingress:
  - service: http_status:404
EOF
```

## 5. Share the declaration directory into all instances

```bash
incus profile device add default svc-declare disk \
    source=/etc/cloudflared/services.d \
    path=/mnt/services
```

## 6. Helper scripts (on the host)

### /usr/local/bin/svc-rebuild

Reads all service declarations and rebuilds the cloudflared ingress config.

```bash
#!/usr/bin/env bash
set -euo pipefail

CONFIG="/etc/cloudflared/config.yml"
SERVICES_DIR="/etc/cloudflared/services.d"
TUNNEL_CREDS="/root/.cloudflared/<TUNNEL_ID>.json"  # fix this

cat > "$CONFIG" <<HEADER
tunnel: ampbox
credentials-file: $TUNNEL_CREDS

ingress:
HEADER

for f in "$SERVICES_DIR"/*.yml; do
    [ -f "$f" ] || continue
    while IFS= read -r line; do
        echo "  $line" >> "$CONFIG"
    done < "$f"
done

echo "  - service: http_status:404" >> "$CONFIG"
sudo systemctl restart cloudflared
echo "Rebuilt with $(ls "$SERVICES_DIR"/*.yml 2>/dev/null | wc -l) service(s)"
```

```bash
sudo install -m 755 /dev/stdin /usr/local/bin/svc-rebuild < svc-rebuild.sh
```

### /usr/local/bin/svc-watch

Auto-rebuilds on file changes (optional but recommended).

```bash
#!/usr/bin/env bash
set -euo pipefail

SERVICES_DIR="/etc/cloudflared/services.d"
echo "Watching $SERVICES_DIR for changes..."
while inotifywait -q -e create,delete,modify "$SERVICES_DIR"; do
    sleep 1  # debounce
    /usr/local/bin/svc-rebuild
done
```

```bash
sudo apt install -y inotify-tools
sudo install -m 755 /dev/stdin /usr/local/bin/svc-watch < svc-watch.sh
```

## 7. Systemd services (on the host)

### /etc/systemd/system/cloudflared.service

```ini
[Unit]
Description=Cloudflare Tunnel
After=network-online.target
Wants=network-online.target

[Service]
ExecStart=/usr/local/bin/cloudflared tunnel run ampbox
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target
```

### /etc/systemd/system/svc-watch.service

```ini
[Unit]
Description=Service declaration watcher
After=cloudflared.service

[Service]
ExecStart=/usr/local/bin/svc-watch
Restart=always

[Install]
WantedBy=multi-user.target
```

```bash
sudo systemctl daemon-reload
sudo systemctl enable --now cloudflared svc-watch
```

## 8. Declaring services FROM INSIDE an instance

### Expose an HTTP service

```bash
cat > /mnt/services/myapp.yml << EOF
- hostname: myapp.ampbox.io
  service: http://$(hostname -I | awk '{print $1}'):8080
EOF
```

### Expose SSH

```bash
cat > /mnt/services/ssh-$(hostname).yml << EOF
- hostname: ssh-$(hostname).ampbox.io
  service: ssh://$(hostname -I | awk '{print $1}'):22
EOF
```

### Remove a service

```bash
rm /mnt/services/myapp.yml
# svc-watch picks it up and rebuilds automatically
```

## 9. Client-side SSH config (on laptops / remote machines)

```
# ~/.ssh/config
Host *.ampbox.io
    ProxyCommand cloudflared access ssh --hostname %h
```

Then: `ssh user@ssh-myinstance.ampbox.io` just works.

## 10. OPNsense cleanup

Once the tunnel is verified working, remove from OPNsense:
- All per-service reverse proxy rules in os-caddy
- Port forward / NAT rules for 80 and 443
- DDNS configuration (no longer needed)

OPNsense goes back to just being your firewall.

## Quick Reference

| Action                  | From inside instance                                     |
|-------------------------|----------------------------------------------------------|
| Expose HTTP             | `echo "- hostname: X.ampbox.io\n  service: http://IP:PORT" > /mnt/services/X.yml` |
| Expose SSH              | Same pattern with `service: ssh://IP:22`                 |
| Expose raw TCP          | Same pattern with `service: tcp://IP:PORT`               |
| Remove service          | `rm /mnt/services/X.yml`                                 |
| List registered         | `ls /mnt/services/`                                      |
| Check tunnel status     | `sudo systemctl status cloudflared` (from host)          |