Fixes discovered during live provisioning: app.ini permissions (root:gitea + 660→640 for token generation), HTTPS via acme.sh DNS-01, setcap for port 443, admin user creation, INSTALL_LOCK lifecycle. Secrets replaced with placeholders. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
179 lines
5.5 KiB
YAML
179 lines
5.5 KiB
YAML
#cloud-config
|
|
#
|
|
# Foxhunt Dev Git Server (Scaleway DEV1-S, nl-ams-1)
|
|
#
|
|
# Prerequisites:
|
|
# - Scaleway security group: drop all inbound (Tailscale-only access)
|
|
# - Public IP attached (Tailscale needs outbound internet for WireGuard tunnel)
|
|
# - DNS: git.fxhnt.ai A record pointing to Tailscale IP
|
|
# - Scaleway DNS API token for Let's Encrypt DNS-01 challenge
|
|
#
|
|
# Lessons learned:
|
|
# - Tailscale REQUIRES outbound internet — cannot remove public IP entirely
|
|
# - Gitea needs write access to app.ini on first start (generates internal token)
|
|
# - setcap needed for non-root port 443 binding
|
|
# - Config file must be owned root:gitea with 660 on first boot, then 640 after
|
|
#
|
|
package_update: true
|
|
package_upgrade: true
|
|
|
|
packages:
|
|
- git
|
|
- sqlite3
|
|
- curl
|
|
- wget
|
|
- socat
|
|
|
|
write_files:
|
|
- path: /etc/gitea/app.ini
|
|
# 660 on first boot so Gitea can write INTERNAL_TOKEN and SECRET_KEY
|
|
# A post-start step tightens this to 640 after tokens are generated
|
|
permissions: '0660'
|
|
content: |
|
|
[server]
|
|
PROTOCOL = https
|
|
DOMAIN = git.fxhnt.ai
|
|
ROOT_URL = https://git.fxhnt.ai/
|
|
HTTP_PORT = 443
|
|
SSH_DOMAIN = git.fxhnt.ai
|
|
START_SSH_SERVER = true
|
|
SSH_PORT = 2222
|
|
SSH_LISTEN_PORT = 2222
|
|
DISABLE_SSH = false
|
|
LFS_START_SERVER = true
|
|
|
|
[database]
|
|
DB_TYPE = sqlite3
|
|
PATH = /var/lib/gitea/data/gitea.db
|
|
|
|
[repository]
|
|
ROOT = /var/lib/gitea/repositories
|
|
|
|
[lfs]
|
|
PATH = /var/lib/gitea/data/lfs
|
|
|
|
[log]
|
|
ROOT_PATH = /var/lib/gitea/log
|
|
MODE = file
|
|
LEVEL = Info
|
|
|
|
[service]
|
|
DISABLE_REGISTRATION = true
|
|
|
|
[security]
|
|
INSTALL_LOCK = false
|
|
|
|
runcmd:
|
|
# --- Tailscale ---
|
|
- curl -fsSL https://tailscale.com/install.sh | sh
|
|
- tailscale up --authkey=TAILSCALE_AUTH_KEY_PLACEHOLDER --hostname=vm-fxhnt-git --ssh
|
|
|
|
# Wait for Tailscale to get an IP (up to 60s)
|
|
- |
|
|
for i in $(seq 1 30); do
|
|
TS_IP=$(tailscale ip -4 2>/dev/null)
|
|
if [ -n "$TS_IP" ]; then
|
|
echo "Tailscale IP: $TS_IP"
|
|
break
|
|
fi
|
|
sleep 2
|
|
done
|
|
|
|
# Bind Gitea to Tailscale interface only and set TLS cert paths
|
|
- |
|
|
TS_IP=$(tailscale ip -4)
|
|
sed -i "/^HTTP_PORT/i HTTP_ADDR = ${TS_IP}" /etc/gitea/app.ini
|
|
# Add cert paths (populated later by acme.sh)
|
|
sed -i "/^LFS_START_SERVER/a CERT_FILE = /etc/gitea/ssl/fullchain.pem\nKEY_FILE = /etc/gitea/ssl/key.pem" /etc/gitea/app.ini
|
|
|
|
# --- Gitea user and directories ---
|
|
- adduser --system --shell /bin/bash --gecos 'Gitea' --group --disabled-password --home /home/gitea gitea
|
|
- mkdir -p /var/lib/gitea/{custom,data,log,repositories}
|
|
- chown -R gitea:gitea /var/lib/gitea
|
|
- chmod -R 750 /var/lib/gitea
|
|
- mkdir -p /etc/gitea
|
|
- chown root:gitea /etc/gitea
|
|
- chmod 770 /etc/gitea
|
|
# Fix app.ini ownership (write_files creates as root:root)
|
|
- chown root:gitea /etc/gitea/app.ini
|
|
|
|
# --- Download and install Gitea ---
|
|
- |
|
|
GITEA_VERSION=$(curl -s https://api.github.com/repos/go-gitea/gitea/releases/latest | grep tag_name | cut -d '"' -f 4 | sed 's/v//')
|
|
wget -O /usr/local/bin/gitea "https://dl.gitea.com/gitea/${GITEA_VERSION}/gitea-${GITEA_VERSION}-linux-amd64"
|
|
chmod +x /usr/local/bin/gitea
|
|
|
|
# Allow Gitea to bind to port 443 without root
|
|
- setcap 'cap_net_bind_service=+ep' /usr/local/bin/gitea
|
|
|
|
# --- TLS cert via acme.sh + Scaleway DNS-01 ---
|
|
- |
|
|
mkdir -p /etc/gitea/ssl
|
|
curl https://get.acme.sh | sh -s email=admin@fxhnt.ai
|
|
export SCW_ACCESS_KEY=SCW_ACCESS_KEY_PLACEHOLDER
|
|
export SCW_SECRET_KEY=SCW_SECRET_KEY_PLACEHOLDER
|
|
/root/.acme.sh/acme.sh --issue --dns dns_scaleway -d git.fxhnt.ai \
|
|
--keylength ec-256 --server letsencrypt
|
|
/root/.acme.sh/acme.sh --install-cert -d git.fxhnt.ai --ecc \
|
|
--cert-file /etc/gitea/ssl/cert.pem \
|
|
--key-file /etc/gitea/ssl/key.pem \
|
|
--fullchain-file /etc/gitea/ssl/fullchain.pem \
|
|
--reloadcmd "systemctl restart gitea"
|
|
chown -R gitea:gitea /etc/gitea/ssl
|
|
chmod 600 /etc/gitea/ssl/key.pem
|
|
|
|
# --- Systemd service ---
|
|
- |
|
|
cat > /etc/systemd/system/gitea.service << 'UNIT'
|
|
[Unit]
|
|
Description=Gitea (git.fxhnt.ai)
|
|
After=syslog.target network.target tailscaled.service
|
|
|
|
[Service]
|
|
RestartSec=2s
|
|
Type=simple
|
|
User=gitea
|
|
Group=gitea
|
|
ExecStart=/usr/local/bin/gitea web --config /etc/gitea/app.ini
|
|
Restart=always
|
|
Environment=USER=gitea HOME=/home/gitea GITEA_WORK_DIR=/var/lib/gitea
|
|
|
|
[Install]
|
|
WantedBy=multi-user.target
|
|
UNIT
|
|
|
|
- systemctl daemon-reload
|
|
- systemctl enable gitea
|
|
|
|
# --- First start: let Gitea generate INTERNAL_TOKEN and SECRET_KEY ---
|
|
- systemctl start gitea
|
|
- |
|
|
# Wait for Gitea to write tokens (up to 30s)
|
|
for i in $(seq 1 15); do
|
|
if grep -q 'INTERNAL_TOKEN' /etc/gitea/app.ini 2>/dev/null; then
|
|
echo "Gitea tokens generated"
|
|
break
|
|
fi
|
|
sleep 2
|
|
done
|
|
|
|
# Tighten config permissions after token generation
|
|
- chmod 640 /etc/gitea/app.ini
|
|
|
|
# Set INSTALL_LOCK now that first boot is done
|
|
- sed -i 's/INSTALL_LOCK = false/INSTALL_LOCK = true/' /etc/gitea/app.ini
|
|
- systemctl restart gitea
|
|
|
|
# --- Create admin user ---
|
|
- |
|
|
sleep 3
|
|
su -c 'GITEA_WORK_DIR=/var/lib/gitea /usr/local/bin/gitea admin user create \
|
|
--config /etc/gitea/app.ini \
|
|
--username foxhunt-admin \
|
|
--password "ADMIN_PASSWORD_PLACEHOLDER" \
|
|
--email admin@fxhnt.ai \
|
|
--admin' - gitea
|
|
|
|
# Signal completion
|
|
- echo "PROVISIONING_COMPLETE" > /var/log/cloud-init-done
|