feat: replace Tailscale VPN gate with simple access code
Remove the vpn-setup/ directory (Headscale/Tailscale scripts, CoreDNS, onboarding) and add a lightweight access code gate instead. Visitors must enter a code to view the site — validated via API route with an httpOnly cookie persisted for 1 year. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
9ea2a0ff9f
commit
228f3c6658
|
|
@ -0,0 +1,22 @@
|
|||
import { NextResponse } from "next/server"
|
||||
|
||||
const ACCESS_CODE = process.env.ACCESS_CODE || "42069"
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const { code } = await request.json()
|
||||
|
||||
if (code !== ACCESS_CODE) {
|
||||
return NextResponse.json({ error: "Invalid code" }, { status: 401 })
|
||||
}
|
||||
|
||||
const response = NextResponse.json({ success: true })
|
||||
response.cookies.set("jefflix-access", "granted", {
|
||||
httpOnly: true,
|
||||
secure: true,
|
||||
sameSite: "lax",
|
||||
maxAge: 60 * 60 * 24 * 365, // 1 year
|
||||
path: "/",
|
||||
})
|
||||
|
||||
return response
|
||||
}
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { JefflixLogo } from "@/components/jefflix-logo"
|
||||
|
||||
export default function GatePage() {
|
||||
const [code, setCode] = useState("")
|
||||
const [error, setError] = useState("")
|
||||
const [loading, setLoading] = useState(false)
|
||||
const router = useRouter()
|
||||
|
||||
async function handleSubmit(e: React.FormEvent) {
|
||||
e.preventDefault()
|
||||
setError("")
|
||||
setLoading(true)
|
||||
|
||||
const res = await fetch("/api/verify-code", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ code }),
|
||||
})
|
||||
|
||||
if (res.ok) {
|
||||
router.push("/")
|
||||
router.refresh()
|
||||
} else {
|
||||
setError("Wrong code. Try again.")
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background flex items-center justify-center">
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-yellow-50/80 via-background/90 to-green-50/80" />
|
||||
<div className="relative w-full max-w-md mx-auto px-4">
|
||||
<div className="text-center space-y-8">
|
||||
<JefflixLogo />
|
||||
<p className="text-lg text-muted-foreground">
|
||||
Enter the access code to continue
|
||||
</p>
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<input
|
||||
type="text"
|
||||
inputMode="numeric"
|
||||
value={code}
|
||||
onChange={(e) => setCode(e.target.value)}
|
||||
placeholder="Access code"
|
||||
autoFocus
|
||||
className="w-full text-center text-2xl tracking-widest px-4 py-3 rounded-lg border-2 border-border bg-card focus:outline-none focus:ring-2 focus:ring-ring"
|
||||
/>
|
||||
{error && (
|
||||
<p className="text-destructive font-medium">{error}</p>
|
||||
)}
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading || !code}
|
||||
className="w-full py-3 px-6 rounded-lg bg-primary text-primary-foreground font-bold text-lg hover:opacity-90 transition-opacity disabled:opacity-50"
|
||||
>
|
||||
{loading ? "Checking..." : "Enter"}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
import { NextResponse } from "next/server"
|
||||
import type { NextRequest } from "next/server"
|
||||
|
||||
export function middleware(request: NextRequest) {
|
||||
const hasAccess = request.cookies.get("jefflix-access")?.value === "granted"
|
||||
|
||||
if (!hasAccess) {
|
||||
return NextResponse.redirect(new URL("/gate", request.url))
|
||||
}
|
||||
|
||||
return NextResponse.next()
|
||||
}
|
||||
|
||||
export const config = {
|
||||
matcher: [
|
||||
/*
|
||||
* Match all paths except:
|
||||
* - /gate (the code entry page)
|
||||
* - /api/verify-code (the verification endpoint)
|
||||
* - /_next (Next.js internals)
|
||||
* - /favicon.ico, /icon*, /apple-icon*, /og-image* (static assets)
|
||||
*/
|
||||
"/((?!gate|api/verify-code|_next|favicon\\.ico|icon|apple-icon|og-image).*)",
|
||||
],
|
||||
}
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
# Jefflix VPN Setup — Headscale + Tailscale
|
||||
|
||||
Protects all `*.jefflix.lol` services behind the existing Headscale VPN at `vpn.jeffemmett.com`.
|
||||
|
||||
## How It Works
|
||||
|
||||
```
|
||||
Before (public):
|
||||
Browser → Cloudflare → Tunnel → Traefik → Jellyfin/etc
|
||||
|
||||
After (VPN-only):
|
||||
Browser → Tailscale (WireGuard) → Traefik → Jellyfin/etc
|
||||
(Only works if connected to the tailnet)
|
||||
```
|
||||
|
||||
Traefik still routes by Host header — the only change is how traffic reaches it.
|
||||
|
||||
## Quick Start
|
||||
|
||||
SSH into the server and follow the phases in order:
|
||||
|
||||
```bash
|
||||
ssh netcup
|
||||
```
|
||||
|
||||
Then run `setup.sh` (or follow the manual steps below).
|
||||
|
||||
## Files
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `setup.sh` | Full setup script (run on Netcup) |
|
||||
| `coredns/Corefile` | CoreDNS config — resolves *.jefflix.lol to Tailscale IP |
|
||||
| `coredns/docker-compose.yml` | CoreDNS container definition |
|
||||
| `headscale-config-patch.yaml` | Split DNS addition for Headscale config |
|
||||
| `cloudflared-config-clean.yml` | Cloudflare tunnel config with jefflix entries removed |
|
||||
| `rollback.sh` | Emergency rollback script |
|
||||
|
|
@ -1,99 +0,0 @@
|
|||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
# ============================================================
|
||||
# Jefflix VPN Cutover Script
|
||||
# Removes public access to *.jefflix.lol
|
||||
# Run ONLY after setup.sh and testing VPN access works
|
||||
# ============================================================
|
||||
|
||||
echo "========================================"
|
||||
echo " Jefflix Public Access Cutover"
|
||||
echo "========================================"
|
||||
echo ""
|
||||
|
||||
# Pre-flight check: verify VPN access works
|
||||
TAILSCALE_IP=$(tailscale ip -4 2>/dev/null || echo "")
|
||||
if [ -z "$TAILSCALE_IP" ]; then
|
||||
echo "ERROR: Tailscale not running on this server. Run setup.sh first."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Server Tailscale IP: $TAILSCALE_IP"
|
||||
echo ""
|
||||
|
||||
# Check DNS works
|
||||
DIG_RESULT=$(dig +short @${TAILSCALE_IP} movies.jefflix.lol 2>/dev/null || echo "FAILED")
|
||||
if [ "$DIG_RESULT" != "$TAILSCALE_IP" ]; then
|
||||
echo "ERROR: CoreDNS not resolving correctly. Got: $DIG_RESULT (expected $TAILSCALE_IP)"
|
||||
echo "Fix CoreDNS before proceeding."
|
||||
exit 1
|
||||
fi
|
||||
echo "✓ CoreDNS resolving correctly"
|
||||
|
||||
echo ""
|
||||
echo "This will REMOVE public access to all *.jefflix.lol services."
|
||||
echo "Users will need Tailscale connected to vpn.jeffemmett.com to access Jefflix."
|
||||
echo ""
|
||||
read -p "Continue? [y/N] " -n 1 -r
|
||||
echo ""
|
||||
|
||||
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
|
||||
echo "Aborted."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# --- Remove jefflix entries from Cloudflare tunnel ---
|
||||
echo ""
|
||||
echo "[Cutover] Removing *.jefflix.lol from Cloudflare tunnel..."
|
||||
|
||||
TUNNEL_CONFIG="/root/cloudflared/config.yml"
|
||||
|
||||
# Backup current config (timestamped)
|
||||
cp "$TUNNEL_CONFIG" "${TUNNEL_CONFIG}.pre-cutover-$(date +%Y%m%d-%H%M%S)"
|
||||
|
||||
# Remove all jefflix.lol hostname entries (hostname + service lines)
|
||||
# This removes the "- hostname: *.jefflix.lol" and its " service:" line
|
||||
python3 -c "
|
||||
import yaml, sys
|
||||
|
||||
with open('$TUNNEL_CONFIG', 'r') as f:
|
||||
config = yaml.safe_load(f)
|
||||
|
||||
original_count = len(config.get('ingress', []))
|
||||
|
||||
# Filter out jefflix.lol entries
|
||||
config['ingress'] = [
|
||||
entry for entry in config.get('ingress', [])
|
||||
if not (isinstance(entry.get('hostname', ''), str) and 'jefflix.lol' in entry.get('hostname', ''))
|
||||
]
|
||||
|
||||
removed = original_count - len(config['ingress'])
|
||||
|
||||
with open('$TUNNEL_CONFIG', 'w') as f:
|
||||
yaml.dump(config, f, default_flow_style=False, sort_keys=False)
|
||||
|
||||
print(f' Removed {removed} jefflix.lol entries from tunnel config')
|
||||
"
|
||||
|
||||
# Restart cloudflared
|
||||
echo " Restarting cloudflared..."
|
||||
docker restart cloudflared
|
||||
echo " ✓ Cloudflared restarted"
|
||||
|
||||
# Wait and verify
|
||||
sleep 5
|
||||
echo ""
|
||||
echo "========================================"
|
||||
echo " Cutover Complete"
|
||||
echo "========================================"
|
||||
echo ""
|
||||
echo "Public access to *.jefflix.lol is now REMOVED."
|
||||
echo ""
|
||||
echo "Verify from a non-VPN device:"
|
||||
echo " curl -I https://movies.jefflix.lol (should fail/404)"
|
||||
echo ""
|
||||
echo "Verify from a VPN device:"
|
||||
echo " curl http://movies.jefflix.lol (should work)"
|
||||
echo ""
|
||||
echo "To ROLLBACK: run rollback.sh"
|
||||
|
|
@ -1,66 +0,0 @@
|
|||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
# ============================================================
|
||||
# Generate a Jefflix VPN invite for a new user
|
||||
# Usage: ./onboard-user.sh [username]
|
||||
# ============================================================
|
||||
|
||||
USERNAME="${1:-}"
|
||||
|
||||
if [ -z "$USERNAME" ]; then
|
||||
echo "Usage: ./onboard-user.sh <username>"
|
||||
echo " Creates a pre-auth key for the user and prints setup instructions."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Generate a single-use pre-auth key (7 day expiry)
|
||||
echo "Generating pre-auth key for user: $USERNAME"
|
||||
PREAUTH_KEY=$(docker exec headscale headscale preauthkeys create \
|
||||
--user jefflix \
|
||||
--reusable=false \
|
||||
--expiration 168h 2>&1 | tail -1)
|
||||
|
||||
echo ""
|
||||
echo "============================================================"
|
||||
echo " Send the following to $USERNAME:"
|
||||
echo "============================================================"
|
||||
echo ""
|
||||
cat <<MSG
|
||||
Hey! Here's how to connect to Jefflix:
|
||||
|
||||
1. Install Tailscale on your device:
|
||||
- Windows/Mac/Linux: https://tailscale.com/download
|
||||
- iOS: Search "Tailscale" in the App Store
|
||||
- Android: Search "Tailscale" in Google Play
|
||||
|
||||
2. Connect to the Jefflix VPN:
|
||||
|
||||
Desktop (open a terminal and run):
|
||||
tailscale up --login-server=https://vpn.jeffemmett.com --authkey=$PREAUTH_KEY
|
||||
|
||||
iOS: Settings → tap account → "..." → Use custom coordination server
|
||||
Server: https://vpn.jeffemmett.com
|
||||
Then log in with this key: $PREAUTH_KEY
|
||||
|
||||
Android: Settings → tap account → "..." → Use alternate server
|
||||
Server: https://vpn.jeffemmett.com
|
||||
Then log in with this key: $PREAUTH_KEY
|
||||
|
||||
3. Once connected, open your browser and go to:
|
||||
- http://movies.jefflix.lol (Watch movies & shows)
|
||||
- http://requests.jefflix.lol (Request new content)
|
||||
- http://upload.jefflix.lol (Upload your own content)
|
||||
- http://music.jefflix.lol (Listen to music)
|
||||
|
||||
Tailscale runs in the background — you only need to set it up once!
|
||||
|
||||
NOTE: This invite key expires in 7 days. Let me know if you need a new one.
|
||||
MSG
|
||||
|
||||
echo ""
|
||||
echo "============================================================"
|
||||
echo " Key: $PREAUTH_KEY"
|
||||
echo " Expires: 7 days"
|
||||
echo " User: jefflix"
|
||||
echo "============================================================"
|
||||
|
|
@ -1,40 +0,0 @@
|
|||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
# ============================================================
|
||||
# Jefflix VPN Rollback Script
|
||||
# Restores public access to *.jefflix.lol
|
||||
# ============================================================
|
||||
|
||||
echo "========================================"
|
||||
echo " Jefflix VPN Rollback"
|
||||
echo "========================================"
|
||||
echo ""
|
||||
|
||||
# Restore Cloudflare tunnel config
|
||||
if [ -f /root/cloudflared/config.yml.backup-jefflix-vpn ]; then
|
||||
cp /root/cloudflared/config.yml.backup-jefflix-vpn /root/cloudflared/config.yml
|
||||
docker restart cloudflared
|
||||
echo "✓ Cloudflare tunnel config restored and restarted"
|
||||
else
|
||||
echo "⚠ No backup found at /root/cloudflared/config.yml.backup-jefflix-vpn"
|
||||
fi
|
||||
|
||||
# Restore Headscale config
|
||||
if [ -f /opt/apps/headscale-deploy/config/config.yaml.backup-jefflix-vpn ]; then
|
||||
cp /opt/apps/headscale-deploy/config/config.yaml.backup-jefflix-vpn /opt/apps/headscale-deploy/config/config.yaml
|
||||
cd /opt/apps/headscale-deploy && docker compose restart headscale
|
||||
echo "✓ Headscale config restored and restarted"
|
||||
else
|
||||
echo "⚠ No Headscale backup found"
|
||||
fi
|
||||
|
||||
# Stop CoreDNS (optional — it doesn't hurt to leave it running)
|
||||
if docker ps --format '{{.Names}}' | grep -q jefflix-dns; then
|
||||
cd /opt/apps/jefflix-dns && docker compose down
|
||||
echo "✓ CoreDNS stopped"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "Rollback complete. Public access to *.jefflix.lol should be restored."
|
||||
echo "Verify: curl -I https://movies.jefflix.lol"
|
||||
|
|
@ -1,151 +0,0 @@
|
|||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
# ============================================================
|
||||
# Jefflix VPN Setup Script
|
||||
# Run this on the Netcup RS 8000 server (ssh netcup)
|
||||
# ============================================================
|
||||
|
||||
echo "========================================"
|
||||
echo " Jefflix VPN Setup (Headscale/Tailscale)"
|
||||
echo "========================================"
|
||||
echo ""
|
||||
|
||||
# --- Phase 1: Backups ---
|
||||
echo "[Phase 1] Creating backups..."
|
||||
cp /root/cloudflared/config.yml /root/cloudflared/config.yml.backup-jefflix-vpn
|
||||
cp /opt/apps/headscale-deploy/config/config.yaml /opt/apps/headscale-deploy/config/config.yaml.backup-jefflix-vpn
|
||||
cp -r /root/traefik/config/ /root/traefik/config-backup-jefflix-vpn/ 2>/dev/null || true
|
||||
echo " ✓ Backups created"
|
||||
|
||||
# --- Phase 1b: Create Headscale users ---
|
||||
echo ""
|
||||
echo "[Phase 1b] Creating Headscale users..."
|
||||
docker exec headscale headscale users create server 2>/dev/null || echo " (user 'server' already exists)"
|
||||
docker exec headscale headscale users create jefflix 2>/dev/null || echo " (user 'jefflix' already exists)"
|
||||
echo " ✓ Users ready"
|
||||
|
||||
# --- Phase 2: Install Tailscale ---
|
||||
echo ""
|
||||
echo "[Phase 2] Installing Tailscale..."
|
||||
if command -v tailscale &>/dev/null; then
|
||||
echo " Tailscale already installed: $(tailscale version)"
|
||||
else
|
||||
curl -fsSL https://tailscale.com/install.sh | sh
|
||||
echo " ✓ Tailscale installed"
|
||||
fi
|
||||
|
||||
# Generate pre-auth key and join tailnet
|
||||
echo ""
|
||||
echo " Generating pre-auth key..."
|
||||
PREAUTH_KEY=$(docker exec headscale headscale preauthkeys create --user server --reusable=false --expiration 1h 2>&1 | tail -1)
|
||||
echo " Pre-auth key: $PREAUTH_KEY"
|
||||
|
||||
echo ""
|
||||
echo " Joining tailnet..."
|
||||
tailscale up \
|
||||
--login-server=https://vpn.jeffemmett.com \
|
||||
--authkey="$PREAUTH_KEY" \
|
||||
--hostname=netcup-rs8000 \
|
||||
--accept-dns=false
|
||||
|
||||
# Get the Tailscale IP
|
||||
TAILSCALE_IP=$(tailscale ip -4)
|
||||
echo " ✓ Joined tailnet with IP: $TAILSCALE_IP"
|
||||
|
||||
# --- Phase 3: Deploy CoreDNS ---
|
||||
echo ""
|
||||
echo "[Phase 3] Deploying CoreDNS for *.jefflix.lol..."
|
||||
mkdir -p /opt/apps/jefflix-dns
|
||||
|
||||
# Write Corefile with the actual Tailscale IP
|
||||
cat > /opt/apps/jefflix-dns/Corefile <<COREFILE_EOF
|
||||
jefflix.lol {
|
||||
template IN A {
|
||||
answer "{{ .Name }} 60 IN A ${TAILSCALE_IP}"
|
||||
}
|
||||
log
|
||||
}
|
||||
COREFILE_EOF
|
||||
|
||||
cat > /opt/apps/jefflix-dns/docker-compose.yml <<COMPOSE_EOF
|
||||
services:
|
||||
coredns:
|
||||
image: coredns/coredns:latest
|
||||
container_name: jefflix-dns
|
||||
restart: unless-stopped
|
||||
command: -conf /etc/coredns/Corefile
|
||||
volumes:
|
||||
- ./Corefile:/etc/coredns/Corefile:ro
|
||||
ports:
|
||||
- "${TAILSCALE_IP}:53:53/udp"
|
||||
- "${TAILSCALE_IP}:53:53/tcp"
|
||||
cap_drop:
|
||||
- ALL
|
||||
cap_add:
|
||||
- NET_BIND_SERVICE
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
COMPOSE_EOF
|
||||
|
||||
cd /opt/apps/jefflix-dns && docker compose up -d
|
||||
echo " ✓ CoreDNS deployed"
|
||||
|
||||
# Test DNS
|
||||
echo ""
|
||||
echo " Testing DNS resolution..."
|
||||
sleep 2
|
||||
DIG_RESULT=$(dig +short @${TAILSCALE_IP} movies.jefflix.lol 2>/dev/null || echo "FAILED")
|
||||
if [ "$DIG_RESULT" = "$TAILSCALE_IP" ]; then
|
||||
echo " ✓ DNS test passed: movies.jefflix.lol -> $TAILSCALE_IP"
|
||||
else
|
||||
echo " ⚠ DNS test returned: $DIG_RESULT (expected $TAILSCALE_IP)"
|
||||
echo " Check CoreDNS logs: docker logs jefflix-dns"
|
||||
fi
|
||||
|
||||
# --- Phase 4: Configure Headscale split DNS ---
|
||||
echo ""
|
||||
echo "[Phase 4] Configuring Headscale split DNS..."
|
||||
|
||||
HEADSCALE_CONFIG="/opt/apps/headscale-deploy/config/config.yaml"
|
||||
|
||||
# Check if split DNS is already configured
|
||||
if grep -q "jefflix.lol" "$HEADSCALE_CONFIG"; then
|
||||
echo " Split DNS for jefflix.lol already in config, skipping"
|
||||
else
|
||||
# Add split DNS section to the nameservers block
|
||||
# This uses sed to add the split DNS config after the global nameservers
|
||||
sed -i '/nameservers:/,/^[^ ]/ {
|
||||
/global:/,/^ [^ ]/ {
|
||||
/- 1.0.0.1/a\ split:\n jefflix.lol:\n - '"${TAILSCALE_IP}"'
|
||||
}
|
||||
}' "$HEADSCALE_CONFIG"
|
||||
echo " ✓ Split DNS added to Headscale config"
|
||||
fi
|
||||
|
||||
# Restart Headscale
|
||||
cd /opt/apps/headscale-deploy && docker compose restart headscale
|
||||
echo " ✓ Headscale restarted"
|
||||
sleep 3
|
||||
|
||||
# Verify Headscale is healthy
|
||||
docker exec headscale headscale nodes list >/dev/null 2>&1 && echo " ✓ Headscale healthy" || echo " ⚠ Headscale may need attention"
|
||||
|
||||
echo ""
|
||||
echo "========================================"
|
||||
echo " Setup Complete (Phases 1-4)"
|
||||
echo "========================================"
|
||||
echo ""
|
||||
echo "Server Tailscale IP: $TAILSCALE_IP"
|
||||
echo "CoreDNS: running on $TAILSCALE_IP:53"
|
||||
echo "Split DNS: jefflix.lol -> $TAILSCALE_IP"
|
||||
echo ""
|
||||
echo "NEXT STEPS:"
|
||||
echo " 1. Connect YOUR device to the tailnet and test:"
|
||||
echo " tailscale up --login-server=https://vpn.jeffemmett.com"
|
||||
echo " dig movies.jefflix.lol (should return $TAILSCALE_IP)"
|
||||
echo " curl http://movies.jefflix.lol (should return Jellyfin)"
|
||||
echo ""
|
||||
echo " 2. Once confirmed working, run cutover.sh to remove public access"
|
||||
echo " 3. Then onboard users with pre-auth keys"
|
||||
echo ""
|
||||
Loading…
Reference in New Issue