From e831176dec10a233823eccfed993dde75ad534cb Mon Sep 17 00:00:00 2001 From: Alexander Whitestone Date: Sun, 22 Mar 2026 18:39:11 -0400 Subject: [PATCH] feat: add Nginx + Let's Encrypt deploy config for Gitea TLS Stage reverse proxy configuration and automated deploy script for securing the Gitea instance with TLS. Includes: - Nginx config with HTTPS redirect, HSTS, WebSocket support - One-command deploy script (setup-gitea-tls.sh) that installs Nginx + Certbot, obtains cert, patches app.ini, blocks port 3000 - app.ini hardening reference from security audit (#971) Requires DNS A record for git.alexanderwhitestone.com -> 143.198.27.163 before running the deploy script on the server. Fixes #989 Co-Authored-By: Claude Opus 4.6 --- deploy/gitea-app-ini.patch | 50 +++++++ deploy/nginx-gitea.conf | 75 ++++++++++ deploy/setup-gitea-tls.sh | 299 +++++++++++++++++++++++++++++++++++++ 3 files changed, 424 insertions(+) create mode 100644 deploy/gitea-app-ini.patch create mode 100644 deploy/nginx-gitea.conf create mode 100755 deploy/setup-gitea-tls.sh diff --git a/deploy/gitea-app-ini.patch b/deploy/gitea-app-ini.patch new file mode 100644 index 00000000..c8b92b23 --- /dev/null +++ b/deploy/gitea-app-ini.patch @@ -0,0 +1,50 @@ +# ── Gitea app.ini Hardening Patch ──────────────────────────────────────────── +# +# Apply these changes to /etc/gitea/app.ini (or custom/conf/app.ini) +# AFTER running setup-gitea-tls.sh, or apply manually. +# +# The deploy script handles DOMAIN, ROOT_URL, HTTP_ADDR, and COOKIE_SECURE +# automatically. This file documents the FULL recommended hardening config +# from the security audit (#971). +# +# ── Instructions ──────────────────────────────────────────────────────────── +# +# 1. Back up your current app.ini: +# cp /etc/gitea/app.ini /etc/gitea/app.ini.bak +# +# 2. Apply each section below by editing app.ini. +# +# 3. Restart Gitea: +# systemctl restart gitea +# # or: docker restart gitea + +# ── [server] section ─────────────────────────────────────────────────────── +# These are set automatically by setup-gitea-tls.sh: +# +# DOMAIN = git.alexanderwhitestone.com +# HTTP_ADDR = 127.0.0.1 +# HTTP_PORT = 3000 +# PROTOCOL = http +# ROOT_URL = https://git.alexanderwhitestone.com/ +# +# Additionally recommended: +# ENABLE_PPROF = false +# OFFLINE_MODE = true + +# ── [security] section ───────────────────────────────────────────────────── +# INSTALL_LOCK = true +# SECRET_KEY = +# REVERSE_PROXY_TRUST_LOCAL = true +# COOKIE_SECURE = true (set by deploy script) +# SET_COOKIE_HTTP_ONLY = true + +# ── [service] section ────────────────────────────────────────────────────── +# DISABLE_REGISTRATION = true +# ALLOW_ONLY_EXTERNAL_REGISTRATION = false +# SHOW_REGISTRATION_BUTTON = false +# ENABLE_REVERSE_PROXY_AUTHENTICATION = false +# REQUIRE_SIGNIN_VIEW = true + +# ── [repository] section ─────────────────────────────────────────────────── +# FORCE_PRIVATE = true +# DEFAULT_PRIVATE = private diff --git a/deploy/nginx-gitea.conf b/deploy/nginx-gitea.conf new file mode 100644 index 00000000..3a6f2992 --- /dev/null +++ b/deploy/nginx-gitea.conf @@ -0,0 +1,75 @@ +# ── Gitea Reverse Proxy — TLS via Let's Encrypt ───────────────────────────── +# +# Install path: /etc/nginx/sites-available/gitea +# Symlink: ln -s /etc/nginx/sites-available/gitea /etc/nginx/sites-enabled/ +# +# Prerequisites: +# - DNS A record: git.alexanderwhitestone.com -> 143.198.27.163 +# - certbot + python3-certbot-nginx installed +# - Certificate obtained via: certbot --nginx -d git.alexanderwhitestone.com +# +# After certbot runs, it will auto-modify the ssl lines below. +# This config is the pre-certbot template that certbot enhances. + +# ── HTTP → HTTPS redirect ─────────────────────────────────────────────────── +server { + listen 80; + listen [::]:80; + server_name git.alexanderwhitestone.com; + + # Let's Encrypt ACME challenge + location /.well-known/acme-challenge/ { + root /var/www/html; + } + + location / { + return 301 https://$host$request_uri; + } +} + +# ── HTTPS — reverse proxy to Gitea ────────────────────────────────────────── +server { + listen 443 ssl http2; + listen [::]:443 ssl http2; + server_name git.alexanderwhitestone.com; + + # ── TLS (managed by certbot) ──────────────────────────────────────────── + ssl_certificate /etc/letsencrypt/live/git.alexanderwhitestone.com/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/git.alexanderwhitestone.com/privkey.pem; + + # ── TLS hardening ─────────────────────────────────────────────────────── + ssl_protocols TLSv1.2 TLSv1.3; + ssl_ciphers HIGH:!aNULL:!MD5; + ssl_prefer_server_ciphers on; + ssl_session_cache shared:SSL:10m; + ssl_session_timeout 10m; + + # ── Security headers ──────────────────────────────────────────────────── + add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; + add_header X-Content-Type-Options nosniff always; + add_header X-Frame-Options SAMEORIGIN always; + add_header Referrer-Policy strict-origin-when-cross-origin always; + + # ── Proxy to Gitea ────────────────────────────────────────────────────── + location / { + proxy_pass http://127.0.0.1:3000; + + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + # WebSocket support (for live updates) + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + + # Large repo pushes + client_max_body_size 512m; + + # Timeouts for large git operations + proxy_connect_timeout 300; + proxy_send_timeout 300; + proxy_read_timeout 300; + } +} diff --git a/deploy/setup-gitea-tls.sh b/deploy/setup-gitea-tls.sh new file mode 100755 index 00000000..1be1cde8 --- /dev/null +++ b/deploy/setup-gitea-tls.sh @@ -0,0 +1,299 @@ +#!/usr/bin/env bash +set -euo pipefail + +# ── Gitea TLS Setup — Nginx + Let's Encrypt ───────────────────────────────── +# +# Sets up a reverse proxy with automatic TLS for the Gitea instance. +# +# Prerequisites: +# - Ubuntu/Debian server with root access +# - DNS A record pointing to this server's IP +# - Gitea running on localhost:3000 +# +# Usage: +# sudo bash deploy/setup-gitea-tls.sh git.alexanderwhitestone.com +# sudo bash deploy/setup-gitea-tls.sh git.alexanderwhitestone.com --email admin@alexanderwhitestone.com +# +# What it does: +# 1. Installs Nginx + Certbot +# 2. Deploys the Nginx reverse proxy config +# 3. Obtains a Let's Encrypt TLS certificate +# 4. Patches Gitea app.ini for HTTPS +# 5. Blocks direct access to port 3000 +# 6. Restarts services + +BOLD='\033[1m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +RED='\033[0;31m' +CYAN='\033[0;36m' +NC='\033[0m' + +info() { echo -e "${GREEN}[+]${NC} $1"; } +warn() { echo -e "${YELLOW}[!]${NC} $1"; } +error() { echo -e "${RED}[x]${NC} $1"; } +step() { echo -e "\n${BOLD}── $1 ──${NC}"; } + +# ── Parse arguments ───────────────────────────────────────────────────────── + +DOMAIN="" +EMAIL="" +GITEA_INI="/etc/gitea/app.ini" +DRY_RUN=false + +while [[ $# -gt 0 ]]; do + case $1 in + --email) EMAIL="$2"; shift 2 ;; + --ini) GITEA_INI="$2"; shift 2 ;; + --dry-run) DRY_RUN=true; shift ;; + -*) error "Unknown option: $1"; exit 1 ;; + *) DOMAIN="$1"; shift ;; + esac +done + +if [ -z "$DOMAIN" ]; then + error "Usage: $0 [--email you@example.com] [--ini /path/to/app.ini]" + exit 1 +fi + +if [ -z "$EMAIL" ]; then + EMAIL="admin@${DOMAIN#*.}" +fi + +echo -e "${CYAN}${BOLD}" +echo " ╔══════════════════════════════════════════╗" +echo " ║ Gitea TLS Setup ║" +echo " ║ Nginx + Let's Encrypt ║" +echo " ╚══════════════════════════════════════════╝" +echo -e "${NC}" +echo " Domain: $DOMAIN" +echo " Email: $EMAIL" +echo " Gitea INI: $GITEA_INI" +echo " Dry run: $DRY_RUN" +echo "" + +# ── Preflight checks ─────────────────────────────────────────────────────── + +if [ "$(id -u)" -ne 0 ]; then + error "This script must be run as root (or with sudo)" + exit 1 +fi + +# Verify DNS resolves to this server +step "Checking DNS" +RESOLVED_IP=$(dig +short "$DOMAIN" 2>/dev/null | head -1) +LOCAL_IP=$(curl -4sf https://ifconfig.me 2>/dev/null || hostname -I 2>/dev/null | awk '{print $1}') + +if [ -z "$RESOLVED_IP" ]; then + error "DNS record for $DOMAIN not found." + error "Create an A record pointing $DOMAIN to $LOCAL_IP first." + exit 1 +fi + +if [ "$RESOLVED_IP" != "$LOCAL_IP" ]; then + warn "DNS for $DOMAIN resolves to $RESOLVED_IP but this server is $LOCAL_IP" + warn "Let's Encrypt will fail if DNS doesn't point here. Continue anyway? [y/N]" + read -r CONTINUE + if [ "$CONTINUE" != "y" ] && [ "$CONTINUE" != "Y" ]; then + exit 1 + fi +fi +info "DNS OK: $DOMAIN -> $RESOLVED_IP" + +# Verify Gitea is running +step "Checking Gitea" +if curl -sf http://127.0.0.1:3000/ > /dev/null 2>&1; then + info "Gitea is running on localhost:3000" +else + warn "Gitea not responding on localhost:3000 — continuing anyway" +fi + +if $DRY_RUN; then + info "Dry run — would install nginx, certbot, configure TLS for $DOMAIN" + exit 0 +fi + +# ── Step 1: Install Nginx + Certbot ──────────────────────────────────────── + +step "Installing Nginx + Certbot" +apt-get update -qq +apt-get install -y -qq nginx certbot python3-certbot-nginx +info "Nginx + Certbot installed" + +# ── Step 2: Deploy Nginx config ──────────────────────────────────────────── + +step "Deploying Nginx Configuration" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +NGINX_CONF="$SCRIPT_DIR/nginx-gitea.conf" + +if [ ! -f "$NGINX_CONF" ]; then + error "nginx-gitea.conf not found at $NGINX_CONF" + exit 1 +fi + +# Install config (replacing domain if different) +sed "s/git\.alexanderwhitestone\.com/$DOMAIN/g" "$NGINX_CONF" \ + > /etc/nginx/sites-available/gitea + +ln -sf /etc/nginx/sites-available/gitea /etc/nginx/sites-enabled/gitea + +# Remove default site if it conflicts +if [ -L /etc/nginx/sites-enabled/default ]; then + rm /etc/nginx/sites-enabled/default + info "Removed default Nginx site" +fi + +# Test config (will fail on missing cert — that's expected pre-certbot) +# First deploy without SSL, get cert, then enable SSL +cat > /etc/nginx/sites-available/gitea < /etc/nginx/sites-available/gitea + +nginx -t && systemctl reload nginx +info "Full TLS proxy config deployed" + +# ── Step 4: Patch Gitea app.ini ───────────────────────────────────────────── + +step "Patching Gitea Configuration" +if [ -f "$GITEA_INI" ]; then + # Backup first + cp "$GITEA_INI" "${GITEA_INI}.bak.$(date +%Y%m%d%H%M%S)" + info "Backed up app.ini" + + # Patch server section + sed -i "s|^DOMAIN\s*=.*|DOMAIN = $DOMAIN|" "$GITEA_INI" + sed -i "s|^ROOT_URL\s*=.*|ROOT_URL = https://$DOMAIN/|" "$GITEA_INI" + sed -i "s|^HTTP_ADDR\s*=.*|HTTP_ADDR = 127.0.0.1|" "$GITEA_INI" + + # Enable secure cookies + if grep -q "^COOKIE_SECURE" "$GITEA_INI"; then + sed -i "s|^COOKIE_SECURE\s*=.*|COOKIE_SECURE = true|" "$GITEA_INI" + else + sed -i "/^\[security\]/a COOKIE_SECURE = true" "$GITEA_INI" + fi + + info "Gitea config patched: DOMAIN=$DOMAIN, ROOT_URL=https://$DOMAIN/, HTTP_ADDR=127.0.0.1" +else + warn "Gitea config not found at $GITEA_INI" + warn "Update manually:" + warn " DOMAIN = $DOMAIN" + warn " ROOT_URL = https://$DOMAIN/" + warn " HTTP_ADDR = 127.0.0.1" +fi + +# ── Step 5: Block direct port 3000 access ─────────────────────────────────── + +step "Blocking Direct Port 3000 Access" +if command -v ufw &> /dev/null; then + ufw deny 3000/tcp 2>/dev/null || true + info "Port 3000 blocked via ufw" +else + # Use iptables as fallback + iptables -A INPUT -p tcp --dport 3000 -j DROP 2>/dev/null || true + info "Port 3000 blocked via iptables (not persistent — install ufw for persistence)" +fi + +# Ensure HTTP/HTTPS are allowed +if command -v ufw &> /dev/null; then + ufw allow 80/tcp 2>/dev/null || true + ufw allow 443/tcp 2>/dev/null || true + ufw allow 22/tcp 2>/dev/null || true +fi + +# ── Step 6: Restart Gitea ─────────────────────────────────────────────────── + +step "Restarting Gitea" +if systemctl is-active --quiet gitea; then + systemctl restart gitea + info "Gitea restarted" +elif docker ps --format '{{.Names}}' | grep -q gitea; then + docker restart "$(docker ps --format '{{.Names}}' | grep gitea | head -1)" + info "Gitea container restarted" +else + warn "Could not auto-restart Gitea — restart it manually" +fi + +# ── Step 7: Verify ────────────────────────────────────────────────────────── + +step "Verifying Deployment" +sleep 3 + +# Check HTTPS +if curl -sf "https://$DOMAIN" > /dev/null 2>&1; then + info "HTTPS is working: https://$DOMAIN" +else + warn "HTTPS check failed — may need a moment to propagate" +fi + +# Check HSTS +HSTS=$(curl -sI "https://$DOMAIN" 2>/dev/null | grep -i "strict-transport-security" || true) +if [ -n "$HSTS" ]; then + info "HSTS header present: $HSTS" +else + warn "HSTS header not detected — check Nginx config" +fi + +# Check HTTP redirect +HTTP_STATUS=$(curl -sI "http://$DOMAIN" 2>/dev/null | head -1 | awk '{print $2}') +if [ "$HTTP_STATUS" = "301" ]; then + info "HTTP->HTTPS redirect working (301)" +else + warn "HTTP redirect returned $HTTP_STATUS (expected 301)" +fi + +# ── Summary ───────────────────────────────────────────────────────────────── + +echo "" +echo -e "${GREEN}${BOLD}" +echo " ╔══════════════════════════════════════════╗" +echo " ║ Gitea TLS Setup Complete! ║" +echo " ╚══════════════════════════════════════════╝" +echo -e "${NC}" +echo "" +echo " Gitea: https://$DOMAIN" +echo "" +echo " Certbot auto-renewal is enabled by default." +echo " Test it: certbot renew --dry-run" +echo "" +echo " To check status:" +echo " nginx -t # test config" +echo " systemctl status nginx # proxy status" +echo " certbot certificates # TLS cert info" +echo "" -- 2.43.0