Fix setup.sh: add -k flag, NSS import for Fedora, correct trust flags, LibreWolf docs

This commit is contained in:
Jarian Cottingham 2026-07-01 04:51:22 +00:00
commit 5d417c20e1
22 changed files with 4348 additions and 0 deletions

19
.env.example Normal file
View File

@ -0,0 +1,19 @@
# CertAuth Environment Variables
# Copy this to .env and fill in your real values
# Admin web login password
ADMIN_PASS=your-secure-admin-password
# YubiKey 1 (Root CA) credentials
YK_ROOT_PIN=your-yk1-pin
YK_ROOT_PUK=your-yk1-puk
# YubiKey 2 (Intermediate CA) credentials
YK_INT_PIN=your-yk2-pin
YK_INT_PUK=your-yk2-puk
# JWT signing secret (generate with: python3 -c "import secrets; print(secrets.token_hex(32))")
JWT_SECRET=your-64-char-hex-secret
# PFX download password
PFX_PASS=certauth

16
.gitignore vendored Normal file
View File

@ -0,0 +1,16 @@
# Environment variables containing real secrets
.env
.env.local
.env.*.local
# Python
__pycache__/
*.py[cod]
*.egg-info/
# OS
.DS_Store
Thumbs.db
# Local overrides
*.local

17
Caddyfile Normal file
View File

@ -0,0 +1,17 @@
{
admin off
}
:443 {
encode gzip
tls /etc/ssl/certauth/tls.pem /etc/ssl/certauth/tls.key
reverse_proxy 127.0.0.1:8000 {
header_up Host {host}
header_up X-Real-IP {remote}
}
}
:80 {
redir https://{host}{uri} permanent
}

348
PLAN.md Normal file
View File

@ -0,0 +1,348 @@
# Certificate Authority Infrastructure Plan
## Overview
Build a secure, self-contained Certificate Authority (CA) infrastructure on an isolated Ubuntu machine (`192.168.8.248`) using two YubiKeys for hardware-secured key storage, with a web-based Key Vault UI for certificate management.
---
## Architecture
```
┌─────────────────────────────────────────────────────┐
│ certauth Machine (192.168.8.248) │
│ │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ YubiKey #1 │ │ YubiKey #2 │ │
│ │ Root CA Key │ │ Intermediate │ │
│ │ (PIN+Touch) │ │ CA Key │ │
│ └──────┬───────┘ │ (PIN+Touch) │ │
│ │ └──────┬───────┘ │
│ │ │ │
│ Root Certificate Intermediate Cert │
│ (signed on YK1) (signed by Root) │
│ │ │
│ ┌─────────▼─────────┐ │
│ │ Certificate API │ │
│ │ + Key Vault UI │ │
│ └─────────┬─────────┘ │
│ │ │
│ ┌─────────▼─────────┐ │
│ │ Certificate DB │ │
│ │ (SQLite/Postgres) │ │
│ └───────────────────┘ │
│ │
│ Firewall: Only local network (192.168.8.0/24) │
│ No outbound internet except Ubuntu package updates │
└─────────────────────────────────────────────────────┘
```
---
## Phase 1: Machine Hardening
### 1.1 Base System Configuration
- Fresh Ubuntu Server installation verification
- Create dedicated `certauth` user with sudo privileges
- Disable root login, enforce key-based SSH
- Configure SSH to only accept connections from local network (192.168.8.0/24)
- Set up UFW firewall:
- Allow SSH (port 22) from 192.168.8.0/24 only
- Allow HTTPS (port 443) for Key Vault UI from 192.168.8.0/24 only
- Allow HTTP (port 80) for redirect from 192.168.8.0/24 only
- Deny all other inbound traffic
- Deny all outbound traffic except:
- Ubuntu package repositories (archive.ubuntu.com, security.ubuntu.com)
- DNS (port 53)
- NTP (port 123)
- Local network traffic (192.168.8.0/24)
### 1.2 System Hardening
- Automatic security updates enabled
- Fail2ban for SSH protection
- Auditd for security auditing
- Remove unnecessary packages and services
- ConfigureAppArmor profiles
- Set up encrypted swap
- Configure secure kernel parameters (sysctl)
### 1.3 YubiKey Preparation
- Install `yubikey-manager`, `pcscd`, `opensc`, `gnutls-bin`
- Verify YubiKey connectivity and PIV applet
- Test both YubiKeys are recognized
---
## Phase 2: YubiKey #1 - Root CA
### 2.1 YubiKey Configuration
- Set PIV PIN (strong, 9-16 chars)
- Set PIV PUK (for PIN reset)
- Set management key
- Configure touch policy to `callback` or `fixed-on` for the signing key slot
- Generate RSA 4096-bit (or EC P-384) key pair **on the YubiKey** (never leaves device)
- Key stored in PIV slot `9c` (signing)
### 2.2 Root Certificate Generation
- Generate self-signed Root CA certificate on YubiKey
- Validity: 20-30 years
- Key usage: Certificate Signing, CRL Signing
- Basic constraints: CA:TRUE, pathlen:1
- Subject: `CN=certauth Root CA, O=Home, C=US` (customizable)
- Store Root CA certificate in `/etc/ssl/ca/root/`
### 2.3 Security
- Root CA certificate is public and stored on disk
- Root private key **never** leaves YubiKey #1
- YubiKey #1 can be physically removed and stored offline when not in use
- All signing operations require PIN + physical touch
---
## Phase 3: YubiKey #2 - Intermediate CA
### 3.1 YubiKey Configuration
- Set PIV PIN (strong, 9-16 chars, different from YK1)
- Set PIV PUK
- Set management key
- Configure touch policy to `callback` or `fixed-on` for signing key slot
- Generate RSA 4096-bit (or EC P-384) key pair **on the YubiKey**
- Key stored in PIV slot `9c` (signing)
### 3.2 Intermediate Certificate Generation
- Generate CSR on YubiKey #2
- Sign CSR using YubiKey #1 (Root CA) — requires PIN + touch on YK1
- Validity: 10-15 years
- Key usage: Certificate Signing, CRL Signing
- Basic constraints: CA:TRUE, pathlen:0
- Subject: `CN=certauth Intermediate CA, O=Home, C=US`
- Store Intermediate CA certificate in `/etc/ssl/ca/intermediate/`
### 3.3 Security
- Intermediate private key **never** leaves YubiKey #2
- All signing operations require PIN + physical touch
- Certificate chain: Root CA → Intermediate CA → Leaf Certificates
---
## Phase 4: Certificate Management API & Key Vault UI
### 4.1 Technology Stack
- **Backend**: Python FastAPI (lightweight, async, good OpenAPI support)
- **Database**: SQLite (simple, file-based, sufficient for this scale)
- **Frontend**: HTMX + Tailwind CSS (minimal JS, server-rendered, fast)
- **Reverse Proxy**: Caddy (automatic HTTPS, simple config)
- **Process Manager**: systemd
### 4.2 API Endpoints
```
Authentication:
POST /api/auth/login - Admin login (username + password + TOTP)
POST /api/auth/logout - Logout
GET /api/auth/verify - Verify API key
Domain Management:
POST /api/domains - Register a new domain (e.g., *.example.com)
GET /api/domains - List all registered domains
GET /api/domains/{id} - Get domain details
DELETE /api/domains/{id} - Revoke domain registration
Certificate Management:
POST /api/certs/request - Request a new certificate for a domain
GET /api/certs - List all certificates
GET /api/certs/{id} - Get certificate details
GET /api/certs/{id}/download - Download certificate + chain
POST /api/certs/{id}/revoke - Revoke a certificate
POST /api/certs/{id}/renew - Renew a certificate
Health & Status:
GET /api/health - Health check
GET /api/yubikeys - YubiKey status (connected, slots)
```
### 4.3 Key Vault UI Features
- Dashboard showing certificate inventory, expiring certs, YubiKey status
- Domain registration form with validation
- Certificate request workflow:
1. Select registered domain
2. Specify SANs (Subject Alternative Names)
3. Choose validity period
4. Confirm request (triggers signing workflow)
- Certificate signing workflow:
1. System generates CSR locally
2. Admin inserts YubiKey #2, enters PIN, touches YubiKey
3. Certificate is signed and stored
4. Certificate + chain available for download
- Certificate download page with PEM files
- Audit log viewer
- Settings page for CA configuration
### 4.4 Signing Workflow (Human-in-the-Loop)
Since YubiKey requires physical touch + PIN, the signing process is semi-interactive:
```
1. Admin requests certificate via UI/API
2. System generates CSR and stores pending request
3. Admin runs: certauth sign --request <id>
- This prompts for YubiKey #2 PIN
- Admin touches YubiKey #2 when prompted
- Certificate is signed by Intermediate CA
4. Signed certificate stored in database
5. Status updated to "issued"
6. Certificate available for download
```
Alternative: Web-based signing using WebAuthn/CTAP2 (future enhancement)
### 4.5 API Key Authentication
- Admin generates API keys via UI
- API keys are scoped (read-only, sign, admin)
- Services authenticate with API key header: `X-API-Key: <key>`
- API keys stored hashed in database
---
## Phase 5: Security Hardening
### 5.1 Application Security
- All API endpoints require authentication
- Rate limiting on API endpoints
- Input validation and sanitization
- SQL injection prevention (parameterized queries)
- CSRF protection for UI
- Content Security Policy headers
- Secure cookie flags (HttpOnly, Secure, SameSite)
### 5.2 Data Protection
- Database encrypted at rest (LUKS encrypted partition)
- Certificate files stored with restricted permissions (root:certauth, 0640)
- No secrets in plaintext logs
- API keys hashed (bcrypt)
- Passwords hashed (bcrypt)
### 5.3 Network Security
- Caddy configured with strong TLS settings
- Only binds to local network interfaces
- No services exposed to internet
- Outbound connections restricted by firewall
### 5.4 Operational Security
- Audit logging of all certificate operations
- Regular backup of certificates and configuration (encrypted)
- Monitoring for failed authentication attempts
- Log rotation and retention policy
---
## Phase 6: Setup Script
### 6.1 Automated Setup Script (`setup-certauth.sh`)
A single script that can provision a fresh Ubuntu machine:
- Validates prerequisites (Ubuntu version, YubiKeys present)
- Runs all hardening steps
- Configures YubiKeys (interactive prompts for PINs)
- Generates CA certificates
- Installs and configures the Certificate API
- Sets up firewall rules
- Creates systemd services
- Outputs configuration summary and credentials
### 6.2 Configuration File (`certauth.conf`)
All configurable values in one file:
- CA subject information
- Certificate validity periods
- Network configuration
- Admin credentials
- YubiKey PIN policies
### 6.3 Documentation
- README with architecture overview
- OPERATIONS.md with daily usage instructions
- TROUBLESHOOTING.md with common issues
- BACKUP.md with backup and restore procedures
---
## Directory Structure
```
certauth/
├── setup-certauth.sh # Main setup script
├── certauth.conf # Configuration file
├── README.md # Project documentation
├── OPERATIONS.md # Operations guide
├── scripts/
│ ├── harden-system.sh # System hardening
│ ├── configure-yubikey.sh # YubiKey setup
│ ├── generate-ca.sh # CA certificate generation
│ ├── install-api.sh # API installation
│ └── configure-firewall.sh # Firewall setup
├── api/
│ ├── main.py # FastAPI application
│ ├── models.py # Database models
│ ├── auth.py # Authentication
│ ├── signing.py # Certificate signing logic
│ ├── templates/ # HTML templates
│ ├── static/ # Static assets
│ └── requirements.txt # Python dependencies
├── systemd/
│ ├── certauth-api.service # API service unit
│ └── certauth-sign.timer # Signing timer unit
├── caddy/
│ └── Caddyfile # Caddy configuration
├── ssl/
│ └── openssl.cnf # OpenSSL configuration
└── tests/
└── test_api.py # API tests
```
---
## Implementation Order
1. **Plan Review** — You review and approve this plan
2. **Machine Hardening** — SSH into machine, verify state, apply hardening
3. **YubiKey Setup** — Configure both YubiKeys with Root and Intermediate CAs
4. **API Development** — Build the Certificate Management API and UI
5. **Integration** — Connect API to YubiKeys for signing
6. **Testing** — End-to-end testing of certificate issuance
7. **Setup Script** — Create reproducible setup script
8. **Documentation** — Final documentation and handover
---
## Security Considerations & Trade-offs
### YubiKey Touch Policy
- `callback`: Prompts user to touch (most flexible, requires polling)
- `fixed-on`: Always requires touch (most secure, slightly slower UX)
- **Recommendation**: `fixed-on` for both keys
### Key Algorithm
- RSA 4096: Wider compatibility, larger keys/certs
- EC P-384: Smaller, faster, modern
- **Recommendation**: RSA 4096 for maximum compatibility with all services/browsers
### Signing Workflow
- Fully automated signing is **not possible** with YubiKey touch requirement
- Admin must be present to touch YubiKey and enter PIN
- This is a **feature**, not a limitation — it provides human-in-the-loop security
- For bulk operations, a CLI tool handles batch signing requests
### Offline Root CA
- YubiKey #1 (Root) can be removed after Intermediate CA is created
- Root is only needed if Intermediate CA key is compromised
- This provides true offline root CA capability
---
## Questions for Confirmation
1. **Key algorithm**: RSA 4096 vs EC P-384? (RSA 4096 recommended for compatibility)
2. **Certificate validity**: Root: 25 years, Intermediate: 15 years, Leaf: configurable per-request?
3. **TOTP for admin login**: Enable two-factor authentication?
4. **Backup strategy**: Encrypted backups to local disk only, or also to network share?
5. **Additional domains**: Any specific domains to pre-register beyond `*.example.com`?
6. **CRL/OCSP**: Do we need Certificate Revocation List or OCSP responder? (Adds complexity)
7. **Admin username**: Should the admin account be `certauth` or a different username?

245
SKILL.md Normal file
View File

@ -0,0 +1,245 @@
# CertAuth Key Vault - API & Web UI Guide
## Overview
CertAuth is a self-contained Certificate Authority running on `192.168.8.248` (accessible as `certauth.ms` on the local network). It uses two YubiKey 5 Nano devices as hardware security modules — one for the Root CA and one for the Intermediate CA — requiring physical touch + PIN to sign certificates.
## Authentication
Two auth methods are supported:
### JWT Token (API)
```bash
# Login
curl -X POST https://certauth.ms/api/token \
-H "Content-Type: application/json" \
-d '{"username":"certauth","password":"CHANGE_ME_ADMIN_PASS"}'
# Response: {"access_token": "eyJ...", "token_type": "bearer"}
# Use token in subsequent requests
curl -H "Authorization: Bearer $TOKEN" https://certauth.ms/api/domains
```
### Cookie Auth (Web UI / curl)
```bash
# Login via web endpoint (sets cookie)
curl -c cookies.txt -L https://certauth.ms/login \
-d "username=certauth&password=CHANGE_ME_ADMIN_PASS"
# Use cookie in subsequent requests
curl -b cookies.txt https://certauth.ms/api/domains/web
```
Or use `-b "token=$TOKEN"` with the token value for cookie-auth endpoints.
## Admin Credentials
- **Username**: `certauth`
- **Password**: `CHANGE_ME_ADMIN_PASS`
- **PFX download password**: `certauth`
## API Endpoints
All endpoints require authentication. The server is at `https://certauth.ms`.
### Health Check
```bash
curl -k https://certauth.ms/api/health
# → {"status":"ok"}
```
### CA Chain Download
```bash
curl -k https://certauth.ms/api/ca-chain -o ca-chain.crt
```
Returns the Intermediate + Root CA chain in PEM format.
---
### Domains
**List domains:**
```bash
curl -H "Authorization: Bearer $TOKEN" https://certauth.ms/api/domains
```
**Register domain (API):**
```bash
curl -X POST https://certauth.ms/api/domains \
-H "Authorization: Bearer $TOKEN" \
-d "name=example.com&description=My+website"
```
**Register domain (web/cookie auth):**
```bash
curl -b cookies.txt -X POST https://certauth.ms/api/domains/web \
-d "name=example.com&description=My+website"
```
---
### Certificates
**List certificates:**
```bash
curl -H "Authorization: Bearer $TOKEN" https://certauth.ms/api/certs
```
**Request certificate (API):**
```bash
curl -X POST https://certauth.ms/api/certs/request \
-H "Authorization: Bearer $TOKEN" \
-d "cn=example.com&sans=example.com%2Cwww.example.com&days=365&domain_id=1"
```
**Request certificate (web/cookie auth):**
```bash
curl -b cookies.txt -X POST https://certauth.ms/api/certs/web/request \
-d "cn=example.com&sans=example.com%2Cwww.example.com&days=365&domain_id=0"
```
Parameters:
- `cn` — Common Name (required)
- `sans` — Comma-separated Subject Alternative Names (URL-encoded)
- `days` — Validity in days (default: 365)
- `domain_id` — Domain ID to link to (0 = auto-match by CN)
**Sign/Issue certificate:**
```bash
curl -X POST https://certauth.ms/api/certs/{cert_id}/sign \
-H "Authorization: Bearer $TOKEN"
```
**Sign/Issue certificate (web/cookie auth):**
```bash
curl -b cookies.txt -X POST https://certauth.ms/api/certs/{cert_id}/sign/web
```
⚠️ **Signing requires physical YubiKey touch + PIN** — this will block until the operator touches YubiKey 2 and enters the PIN.
**Download PEM (cert + chain):**
```bash
curl -b cookies.txt https://certauth.ms/api/certs/{cert_id}/pem -o cert.pem
```
**Download PFX (cert + key + chain, password: `certauth`):**
```bash
curl -b cookies.txt https://certauth.ms/api/certs/{cert_id}/pfx -o cert.pfx
```
---
### History
**View certificate history:**
```bash
curl -b cookies.txt https://certauth.ms/history
```
---
### Setup Scripts
**Bash installer (Linux/macOS):**
```bash
curl -sL https://certauth.ms/setup.sh | sudo bash -s 192.168.8.248
```
**PowerShell installer (Windows):**
```powershell
iwr https://certauth.ms/setup.ps1 -UseBasicParsing | iex
```
## Complete Workflow Example
Here's a full end-to-end example to register a domain, request a cert, issue it, and download:
```bash
# 1. Login and get token
TOKEN=$(curl -s -X POST https://certauth.ms/api/token \
-H "Content-Type: application/json" \
-d '{"username":"certauth","password":"CHANGE_ME_ADMIN_PASS"}' \
| python3 -c "import sys,json; print(json.load(sys.stdin)['access_token'])")
# 2. Register domain
curl -s -X POST https://certauth.ms/api/domains \
-H "Authorization: Bearer $TOKEN" \
-d "name=myapp.local&description=Internal+app"
# 3. Request certificate
CERT_ID=$(curl -s -X POST https://certauth.ms/api/certs/request \
-H "Authorization: Bearer $TOKEN" \
-d "cn=myapp.local&sans=myapp.local%2Cwww.myapp.local&days=365&domain_id=0" \
| python3 -c "import sys,json; print(json.load(sys.stdin)['id'])")
echo "Certificate ID: $CERT_ID"
# 4. Issue certificate (requires YubiKey touch!)
curl -s -X POST https://certauth.ms/api/certs/$CERT_ID/sign \
-H "Authorization: Bearer $TOKEN"
# 5. Download PEM (cert + intermediate + root)
curl -s -H "Authorization: Bearer $TOKEN" \
https://certauth.ms/api/certs/$CERT_ID/pem -o myapp.pem
# 6. Download PFX (cert + key + chain, password: certauth)
curl -s -H "Authorization: Bearer $TOKEN" \
https://certauth.ms/api/certs/$CERT_ID/pfx -o myapp.pfx
# 7. Verify
openssl verify -CAfile ca-chain.crt myapp.pem
```
## Certificate Details
- **Key type**: EC P-384 (SECP384R1)
- **Signature algorithm**: ECDSA-SHA384
- **CA chain**: Intermediate CA (YubiKey 2) → Root CA (YubiKey 1)
- **Extensions**: Server Auth + Client Auth EKU, proper AKI/SKI
- **Validity**: 365 days per leaf cert (configurable)
- **PFX password**: `certauth`
## Installing CA Chain on Clients
The `setup.sh` endpoint serves a self-installing bash script that:
1. Auto-detects the server IP (or prompts)
2. Downloads the CA chain
3. Installs it to the OS trust store
4. Supports: Debian/Ubuntu, RHEL/CentOS/Fedora, Arch, Alpine, macOS
For macOS specifically, the CA is installed to the System Keychain with `security add-trusted-cert`.
For LibreWolf (which disables enterprise roots by default):
- Set `security.enterprise_roots.enabled = true` in `about:config`
- OR import the root CA via `certutil` into LibreWolf's NSS database
## Key Files on Server
| Path | Description |
|------|-------------|
| `/etc/ssl/ca/root/root-ca.crt` | Root CA certificate |
| `/etc/ssl/ca/intermediate/intermediate-ca.crt` | Intermediate CA certificate |
| `/etc/ssl/ca/ca-chain.crt` | Full chain (intermediate + root) |
| `/etc/ssl/ca/issued/` | Issued leaf certificates and keys |
| `/etc/ssl/certauth/tls.pem` | Server cert + chain (for Caddy) |
| `/etc/ssl/certauth/tls.key` | Server private key |
| `/opt/certauth/api/` | API application files |
| `/var/lib/certauth/certauth.db` | SQLite database |
## DNS
- `certauth.ms``192.168.8.248` (served by dnsmasq on the server)
- Also added to router DNS for network-wide resolution
## Troubleshooting
- **SSL_ERROR_BAD_CERT_DOMAIN**: Make sure you're using `https://certauth.ms` not the IP. Firefox/NSS has quirks with IP Address SANs.
- **SEC_ERROR_UNKNOWN_ISSUER**: The CA chain isn't trusted. Install via `setup.sh` or import manually.
- **Signing fails**: YubiKey 2 must be touched and PIN entered when signing.
- **DNS not resolving**: Check that `certauth.ms` is in your router's DNS or AdGuard Home config.

29
api/auth.py Normal file
View File

@ -0,0 +1,29 @@
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
from jose import jwt, JWTError
from datetime import datetime, timedelta
from config import SECRET_KEY, ALGORITHM, ACCESS_TOKEN_EXPIRE_MINUTES
from models import get_db, verify_password
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/token")
def create_access_token(data: dict, expires_delta: timedelta = None):
to_encode = data.copy()
expire = datetime.utcnow() + (expires_delta or timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES))
to_encode.update({"exp": expire})
return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
def get_current_user(token: str = Depends(oauth2_scheme)):
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
username = payload.get("sub")
if username is None:
raise credentials_exception
except JWTError:
raise credentials_exception
return username

18
api/config.py Normal file
View File

@ -0,0 +1,18 @@
import os
YK_ROOT_SERIAL = "35450561"
YK_ROOT_PIN = os.environ.get("YK_ROOT_PIN", "CHANGE_ME_YK1_PIN")
YK_INT_SERIAL = "33930436"
YK_INT_PIN = os.environ.get("YK_INT_PIN", "CHANGE_ME_YK2_PIN")
ROOT_CA_PATH = "/etc/ssl/ca/root/root-ca.crt"
INT_CA_PATH = "/etc/ssl/ca/intermediate/intermediate-ca.crt"
CA_CHAIN_PATH = "/etc/ssl/ca/ca-chain.crt"
ISSUED_DIR = "/etc/ssl/ca/issued"
DB_PATH = "/var/lib/certauth/certauth.db"
SECRET_KEY = os.environ.get("JWT_SECRET", "CHANGE_ME_JWT_SECRET")
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 60
ADMIN_USERNAME = "certauth"
PKCS11_MODULE = "/usr/lib/aarch64-linux-gnu/opensc-pkcs11.so"
YK_PUB_ROOT = "/tmp/yk1-root-pub.pem"
YK_PUB_INT = "/tmp/yk2-int-pub.pem"

482
api/main.py Normal file
View File

@ -0,0 +1,482 @@
import os, sqlite3, datetime, secrets, hashlib, subprocess, json
from fastapi import FastAPI, Request, Depends, HTTPException, Form
from fastapi.responses import HTMLResponse, FileResponse, RedirectResponse, JSONResponse, PlainTextResponse, StreamingResponse
from fastapi.staticfiles import StaticFiles
from pydantic import BaseModel
from jose import jwt
from jinja2 import Environment, FileSystemLoader, select_autoescape
from config import *
from models import get_db, init_db, hash_password, verify_password
from auth import create_access_token, get_current_user
from signing import build_leaf_cert
from cryptography.hazmat.primitives import serialization
app = FastAPI(title="CertAuth Key Vault")
app.mount("/static", StaticFiles(directory="/opt/certauth/api/static"), name="static")
def get_user_from_cookie(request: Request):
token = request.cookies.get("token")
if not token: return None
try: return jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
except: return None
jinja_env = Environment(
loader=FileSystemLoader("/opt/certauth/api/templates"),
autoescape=select_autoescape(["html"])
)
@app.on_event("startup")
def startup():
init_db()
try:
with open(ROOT_CA_PATH) as f: root = f.read()
with open(INT_CA_PATH) as f: inter = f.read()
with open(CA_CHAIN_PATH, "w") as f: f.write(inter + "\n" + root)
except: pass
def render(name, ctx):
return HTMLResponse(jinja_env.get_template(name).render(**ctx))
class LoginRequest(BaseModel):
username: str
password: str
@app.post("/api/token")
async def login(req: LoginRequest):
conn = get_db()
row = conn.execute("SELECT * FROM users WHERE username = ?", (req.username,)).fetchone()
conn.close()
if not row or not verify_password(req.password, row["password_hash"]):
raise HTTPException(401, "Invalid credentials")
token = create_access_token({"sub": req.username})
return {"access_token": token, "token_type": "bearer"}
@app.get("/api/me")
async def me(user: str = Depends(get_current_user)):
return {"username": user}
@app.get("/api/domains")
async def list_domains(user: str = Depends(get_current_user)):
conn = get_db()
rows = conn.execute("SELECT * FROM domains ORDER BY created_at DESC").fetchall()
conn.close()
return [dict(r) for r in rows]
@app.post("/api/domains")
async def create_domain(name: str = Form(...), description: str = Form(""),
user: str = Depends(get_current_user)):
conn = get_db()
cur = conn.cursor()
cur.execute("INSERT INTO domains (name, description, created_by) VALUES (?,?,?)",
(name, description, 1))
conn.commit()
conn.close()
return {"status": "ok"}
@app.get("/api/certs")
async def list_certs(user: str = Depends(get_current_user)):
conn = get_db()
rows = conn.execute("SELECT c.*, d.name as domain_name FROM certificates c LEFT JOIN domains d ON c.domain_id = d.id ORDER BY c.created_at DESC").fetchall()
conn.close()
return [dict(r) for r in rows]
@app.post("/api/certs/request")
async def request_cert(cn: str = Form(...), sans: str = Form(""),
days: int = Form(365), domain_id: int = Form(0),
user: str = Depends(get_current_user)):
conn = get_db()
cur = conn.cursor()
cur.execute("INSERT INTO certificates (domain_id, subject, san, status, created_by) VALUES (?,?,?,?,?)",
(domain_id, cn, sans, "pending", 1))
conn.commit()
cid = cur.lastrowid
conn.close()
return {"status": "ok", "id": cid}
@app.post("/api/certs/{cert_id}/sign")
async def sign_cert(cert_id: int, user: str = Depends(get_current_user)):
conn = get_db()
row = conn.execute("SELECT * FROM certificates WHERE id = ?", (cert_id,)).fetchone()
if not row or row["status"] != "pending":
conn.close()
raise HTTPException(400, "Not found or already signed")
conn.close()
result, err = build_leaf_cert(row["subject"], row["san"], 365)
if err: raise HTTPException(500, f"Signing failed: {err}")
cf = f"/etc/ssl/ca/issued/cert-{result['serial']}.crt"
kf = f"/etc/ssl/ca/issued/cert-{result['serial']}.key"
open(cf, "w").write(result["cert_pem"])
open(kf, "w").write(result["key_pem"])
os.chmod(cf, 0o640); os.chmod(kf, 0o600)
conn = get_db()
conn.execute("UPDATE certificates SET status=?, serial=?, cert_path=?, issued_at=?, expires_at=? WHERE id=?",
("issued", result["serial"], cf, datetime.datetime.now().isoformat(), result["expires_at"], cert_id))
conn.commit(); conn.close()
return {"status": "ok", "serial": result["serial"]}
@app.get("/api/certs/{cert_id}/pem")
async def download_pem(cert_id: int, request: Request = None):
"""Download cert + chain as bundled PEM."""
user = get_user_from_cookie(request)
if not user: raise HTTPException(401, "Login required")
conn = get_db()
row = conn.execute("SELECT * FROM certificates WHERE id = ?", (cert_id,)).fetchone()
conn.close()
if not row or row["status"] != "issued": raise HTTPException(404)
pem_path = f"/var/lib/certauth/tmp/cert-{row['serial']}.pem"
with open(row["cert_path"]) as f:
cert_pem = f.read()
with open(CA_CHAIN_PATH) as f:
chain_pem = f.read()
with open(pem_path, "w") as f:
f.write(cert_pem.rstrip() + "\n" + chain_pem)
return FileResponse(pem_path, media_type="application/x-pem-file", filename=f"cert-{row['serial']}.pem")
@app.get("/api/certs/{cert_id}/pfx")
async def download_pfx(cert_id: int, password: str = "certauth", request: Request = None):
"""Download cert + key + chain as PKCS12/PFX."""
user = get_user_from_cookie(request)
if not user: raise HTTPException(401, "Login required")
conn = get_db()
row = conn.execute("SELECT * FROM certificates WHERE id = ?", (cert_id,)).fetchone()
conn.close()
if not row or row["status"] != "issued": raise HTTPException(404)
kf = row["cert_path"].replace(".crt", ".key")
if not os.path.exists(kf): raise HTTPException(404)
from cryptography.hazmat.primitives.serialization import pkcs12, BestAvailableEncryption
from cryptography import x509
with open(row["cert_path"], "rb") as f:
leaf = x509.load_pem_x509_certificate(f.read())
with open(kf, "rb") as f:
key = serialization.load_pem_private_key(f.read(), password=None)
chain_certs = []
with open(CA_CHAIN_PATH, "rb") as f:
for cert_pem in f.read().split(b"-----END CERTIFICATE-----"):
cert_pem = cert_pem.strip()
if cert_pem:
chain_certs.append(x509.load_pem_x509_certificate(cert_pem + b"\n-----END CERTIFICATE-----"))
pfx_data = pkcs12.serialize_key_and_certificates(
name=row["subject"].encode(),
key=key,
cert=leaf,
cas=chain_certs or None,
encryption_algorithm=BestAvailableEncryption(password.encode())
)
pfx_path = f"/var/lib/certauth/tmp/cert-{row['serial']}.pfx"
with open(pfx_path, "wb") as f:
f.write(pfx_data)
return FileResponse(pfx_path, media_type="application/x-pkcs12", filename=f"cert-{row['serial']}.pfx")
@app.get("/api/health")
async def health(): return {"status": "ok"}
@app.get("/api/ca-chain")
async def ca_chain(): return FileResponse(CA_CHAIN_PATH, filename="ca-chain.crt")
def get_user_from_cookie(request: Request):
token = request.cookies.get("token")
if not token:
return None
try:
return jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
except:
return None
@app.get("/", response_class=HTMLResponse)
async def dashboard(request: Request):
user = get_user_from_cookie(request)
if not user:
return RedirectResponse("/login", status_code=302)
conn = get_db()
certs = conn.execute("SELECT c.*, d.name as domain_name FROM certificates c LEFT JOIN domains d ON c.domain_id = d.id ORDER BY c.created_at DESC LIMIT 20").fetchall()
domains = conn.execute("SELECT * FROM domains").fetchall()
p = conn.execute("SELECT COUNT(*) as c FROM certificates WHERE status = ?", ("pending",)).fetchone()["c"]
i = conn.execute("SELECT COUNT(*) as c FROM certificates WHERE status = ?", ("issued",)).fetchone()["c"]
conn.close()
return render("dashboard.html", {"request": request, "user": user,
"certs": [dict(r) for r in certs], "domains": [dict(r) for r in domains],
"pending": p, "issued": i})
@app.get("/login", response_class=HTMLResponse)
async def login_page(request: Request):
return render("login.html", {"request": request, "error": None})
@app.post("/login")
async def login_post(username: str = Form(...), password: str = Form(...)):
conn = get_db()
row = conn.execute("SELECT * FROM users WHERE username = ?", (username,)).fetchone()
conn.close()
if not row or not verify_password(password, row["password_hash"]):
return render("login.html", {"request": None, "error": "Invalid credentials"})
token = create_access_token({"sub": username})
resp = RedirectResponse("/", status_code=302)
resp.set_cookie("token", token, httponly=True, samesite="lax", path="/")
return resp
@app.post("/api/certs/{cert_id}/sign/web")
async def sign_cert_web(cert_id: int, request: Request = None):
user = get_user_from_cookie(request)
if not user:
return RedirectResponse("/login", status_code=302)
conn = get_db()
row = conn.execute("SELECT * FROM certificates WHERE id = ?", (cert_id,)).fetchone()
if not row or row["status"] != "pending":
conn.close()
raise HTTPException(400, "Not found or already issued")
conn.close()
try:
result, err = build_leaf_cert(row["subject"], row["san"], 365)
if err:
return HTMLResponse(f'<span class="text-red-400">Issue failed: {err}</span>')
cf = f"/etc/ssl/ca/issued/cert-{result['serial']}.crt"
kf = f"/etc/ssl/ca/issued/cert-{result['serial']}.key"
open(cf, "w").write(result["cert_pem"])
open(kf, "w").write(result["key_pem"])
os.chmod(cf, 0o640)
os.chmod(kf, 0o600)
conn2 = get_db()
conn2.execute("UPDATE certificates SET status=?, serial=?, cert_path=?, issued_at=?, expires_at=? WHERE id=?",
("issued", result["serial"], cf, datetime.datetime.now().isoformat(), result["expires_at"], cert_id))
conn2.commit()
conn2.close()
return HTMLResponse(f'<span class="text-green-400">Issued! <a href="/api/certs/{cert_id}/pem" class="underline">PEM</a> | <a href="/api/certs/{cert_id}/pfx" class="underline">PFX</a> | <a href="/certs" class="underline">Refresh</a></span>')
except Exception as ex:
return HTMLResponse(f'<span class="text-red-400">Issue failed: {str(ex)}</span>')
@app.get("/logout")
async def logout():
resp = RedirectResponse("/login", status_code=302)
resp.delete_cookie("token", path="/")
return resp
# --- Web API (cookie auth) ---
@app.post("/api/domains/web")
async def create_domain_web(name: str = Form(...), description: str = Form(""), request: Request = None):
user = get_user_from_cookie(request)
if not user:
return RedirectResponse("/login", status_code=302)
conn = get_db()
cur = conn.cursor()
cur.execute("INSERT INTO domains (name, description, created_by) VALUES (?,?,?)", (name, description, 1))
conn.commit()
conn.close()
return HTMLResponse('<span class="text-green-400">Domain registered! <a href="/domains" class="underline">Refresh</a></span>')
@app.post("/api/certs/web/request")
async def request_cert_web(cn: str = Form(...), sans: str = Form(""), days: int = Form(365), domain_id: int = Form(0), request: Request = None):
user = get_user_from_cookie(request)
if not user:
return RedirectResponse("/login", status_code=302)
conn = get_db()
cur = conn.cursor()
# Look up domain by CN if domain_id not provided
if domain_id == 0:
cur.execute("SELECT id FROM domains WHERE name=?", (cn,))
row = cur.fetchone()
domain_id = row[0] if row else None
cur.execute("INSERT INTO certificates (domain_id, subject, san, status, created_by) VALUES (?,?,?,?,?)", (domain_id, cn, sans, "pending", 1))
conn.commit()
conn.close()
return HTMLResponse('<span class="text-green-400">Certificate requested! Click Issue below. <a href="/certs" class="underline">Refresh</a></span>')
@app.get("/domains", response_class=HTMLResponse)
async def domains_page(request: Request):
user = get_user_from_cookie(request)
if not user: return RedirectResponse("/login", status_code=302)
conn = get_db()
rows = conn.execute("SELECT * FROM domains ORDER BY created_at DESC").fetchall()
conn.close()
return render("domains.html", {"request": request, "user": user, "domains": [dict(r) for r in rows]})
@app.get("/certs", response_class=HTMLResponse)
async def certs_page(request: Request):
user = get_user_from_cookie(request)
if not user: return RedirectResponse("/login", status_code=302)
conn = get_db()
rows = conn.execute("SELECT c.*, d.name as domain_name FROM certificates c LEFT JOIN domains d ON c.domain_id = d.id ORDER BY c.created_at DESC").fetchall()
domains = conn.execute("SELECT * FROM domains").fetchall()
conn.close()
return render("certs.html", {"request": request, "user": user, "certs": [dict(r) for r in rows],
"domains": [dict(r) for r in domains]})
@app.get("/history", response_class=HTMLResponse)
async def history_page(request: Request):
user = get_user_from_cookie(request)
if not user: return RedirectResponse("/login", status_code=302)
conn = get_db()
rows = conn.execute("SELECT c.*, d.name as domain_name FROM certificates c LEFT JOIN domains d ON c.domain_id = d.id ORDER BY c.created_at DESC").fetchall()
conn.close()
return render("history.html", {"request": request, "user": user, "certs": [dict(r) for r in rows]})
@app.get("/setup", response_class=HTMLResponse)
async def setup_page(request: Request):
user = get_user_from_cookie(request)
if not user: return RedirectResponse("/login", status_code=302)
return render("setup.html", {"request": request, "user": user})
@app.get("/setup.sh")
async def setup_sh():
"""One-liner bash setup script for Linux/macOS."""
script = r'''#!/bin/bash
set -e
# CertAuth CA Chain Installer
# Usage: curl -sL http://<certauth-ip>/setup.sh | bash
# curl -sL http://<certauth-ip>/setup.sh | sudo bash
# Auto-detect CertAuth server IP
DETECTED_IP=""
if [[ -n "$1" ]]; then
DETECTED_IP="$1"
elif [[ -n "$CERTAUTH_IP" ]]; then
DETECTED_IP="$CERTAUTH_IP"
else
# Try Linux hostname -I first
DETECTED_IP=$(hostname -I 2>/dev/null | awk '{print $1}') || true
# Fallback: ip route (Linux)
[[ -z "$DETECTED_IP" ]] && DETECTED_IP=$(ip route get 1 2>/dev/null | awk '{print $7}' | head -1) || true
# Fallback: ifconfig (macOS/BSD)
[[ -z "$DETECTED_IP" ]] && DETECTED_IP=$(ifconfig 2>/dev/null | grep -E '^\s+(inet )' | awk '{print $2}' | grep -v '127.0.0.1' | head -1) || true
# Fallback: networksetup (macOS only)
[[ -z "$DETECTED_IP" ]] && DETECTED_IP=$(networksetup -getinfo $(networksetup -listallhardwareports 2>/dev/null | awk '/Hardware Port:/ {getline; gsub(/^[ \t]+/, ""); print}') 2>/dev/null | grep 'IP address:' | awk '{print $3}') || true
fi
# Prompt if auto-detection failed
if [[ -z "$DETECTED_IP" ]]; then
read -r -p "Enter CertAuth server IP (e.g., 192.168.8.248): " DETECTED_IP
fi
CHAIN_URL="http://$DETECTED_IP/api/ca-chain"
echo "Downloading CA chain..."
curl -sLk "$CHAIN_URL" -o /tmp/ca-chain.crt || { echo "Failed to download CA chain from $CHAIN_URL"; exit 1; }
# Detect OS and install
if [[ -f /etc/os-release ]]; then
. /etc/os-release
if [[ "$ID" == "debian" || "$ID" == "ubuntu" || "$ID" == "linuxmint" ]]; then
sudo cp /tmp/ca-chain.crt /usr/local/share/ca-certificates/certauth.crt
sudo update-ca-certificates
echo "✅ CA chain installed (Debian/Ubuntu)"
elif [[ "$ID" == "centos" || "$ID" == "rhel" || "$ID" == "fedora" ]]; then
# Install to system trust store (curl, openssl, etc.)
sudo cp /tmp/ca-chain.crt /etc/pki/ca-trust/source/anchors/certauth.crt
sudo update-ca-trust
# Install root CA to NSS database (Firefox, Thunderbird, etc.)
# The root CA is the self-signed cert (second cert in chain)
if command -v certutil &>/dev/null; then
sudo certutil -D -n "CertAuth Root CA" -d sql:/etc/pki/nssdb/ 2>/dev/null || true
python3 -c "
import re, subprocess
with open('/tmp/ca-chain.crt') as f:
content = f.read()
certs = re.findall(r'-----BEGIN CERTIFICATE-----.*?-----END CERTIFICATE-----', content, re.DOTALL)
# Find the self-signed root cert (subject == issuer)
for cert in certs:
subj = subprocess.run(['openssl', 'x509', '-noout', '-subject'], input=cert, capture_output=True, text=True).stdout
iss = subprocess.run(['openssl', 'x509', '-noout', '-issuer'], input=cert, capture_output=True, text=True).stdout
if subj.replace('subject=', '') == iss.replace('issuer=', ''):
with open('/tmp/certauth-root.crt', 'w') as rf:
rf.write(cert + '\n')
break
" 2>/dev/null
if [[ -f /tmp/certauth-root.crt ]]; then
sudo certutil -A -n "CertAuth Root CA" -t "CT,Cu,Tu" -d sql:/etc/pki/nssdb/ -i /tmp/certauth-root.crt 2>/dev/null
echo "✅ Root CA imported to NSS database"
rm -f /tmp/certauth-root.crt
fi
fi
echo "✅ CA chain installed (RHEL/CentOS/Fedora)"
elif [[ "$ID" == "arch" ]]; then
sudo cp /tmp/ca-chain.crt /etc/ca-certificates/trust-source/anchors/certauth.crt
sudo update-ca-trust
echo "✅ CA chain installed (Arch)"
elif [[ "$ID" == "alpine" ]]; then
sudo cp /tmp/ca-chain.crt /usr/local/share/ca-certificates/certauth.crt
sudo update-ca-certificates
echo "✅ CA chain installed (Alpine)"
else
echo "❌ Unsupported Linux distribution: $ID"
echo " Download /tmp/ca-chain.crt and install manually"
exit 1
fi
elif [[ "$(uname)" == "Darwin" ]]; then
sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain /tmp/ca-chain.crt
echo "✅ CA chain installed (macOS)"
else
echo "❌ Unsupported OS: $(uname -s)"
echo " Download /tmp/ca-chain.crt and install manually"
exit 1
fi
# Verify
VERIFY_URL="${CHAIN_URL%/api/ca-chain}"
if curl -sL "$VERIFY_URL" &>/dev/null; then
echo "🌐 Server at $VERIFY_URL is reachable"
else
echo "⚠️ Server at $VERIFY_URL is not reachable (expected if not on same network)"
fi
rm -f /tmp/ca-chain.crt
echo "Done!"
'''
return PlainTextResponse(script, media_type="text/x-shellscript")
@app.get("/setup.ps1")
async def setup_ps1():
"""PowerShell setup script for Windows."""
script = r'''# CertAuth CA Chain Installer for Windows
# Usage: iex (New-Object Net.WebClient).DownloadString("http://<certauth-ip>/setup.ps1")
# iwr http://<certauth-ip>/setup.ps1 -UseBasicParsing | iex
param([string]$CertAuthIP = "")
if (-not $CertAuthIP) {
# Try to detect from environment or prompt
$CertAuthIP = Read-Host "Enter CertAuth server IP (e.g., 192.168.8.248)"
}
$ChainUrl = "http://$CertAuthIP/api/ca-chain"
$ChainPath = "$env:TEMP\ca-chain.crt"
Write-Host "Downloading CA chain from $ChainUrl ..." -ForegroundColor Cyan
try {
(New-Object Net.WebClient).DownloadFile($ChainUrl, $ChainPath)
} catch {
Write-Host "Failed to download CA chain: $_" -ForegroundColor Red
exit 1
}
# Install to Local Machine Trusted Root store
Write-Host "Installing to Trusted Root Certification Authorities..." -ForegroundColor Cyan
try {
$store = New-Object System.Security.Cryptography.X509Certificates.X509Store(
[System.Security.Cryptography.X509Certificates.StoreName]::Root,
[System.Security.Cryptography.X509Certificates.StoreLocation]::LocalMachine)
$store.Open([System.Security.Cryptography.X509Certificates.OpenFlags]::ReadWrite)
$cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2($ChainPath)
$store.Add($cert)
$store.Close()
Write-Host "CA chain installed successfully!" -ForegroundColor Green
} catch {
Write-Host "Failed to install: $_" -ForegroundColor Red
Write-Host "Run as Administrator and try again." -ForegroundColor Yellow
exit 1
}
# Verify
try {
$response = Invoke-WebRequest -Uri "http://$CertAuthIP/api/health" -TimeoutSec 3 -ErrorAction Stop
Write-Host "Server at http://$CertAuthIP is reachable." -ForegroundColor Green
} catch {
Write-Host "Server at http://$CertAuthIP is not reachable (expected if not on same network)." -ForegroundColor Yellow
}
Remove-Item $ChainPath -Force -ErrorAction SilentlyContinue
Write-Host "Done!" -ForegroundColor Green
'''
return PlainTextResponse(script, media_type="text/plain")

77
api/models.py Normal file
View File

@ -0,0 +1,77 @@
import sqlite3, datetime, secrets, bcrypt, os
from config import DB_PATH, ADMIN_USERNAME
def get_db():
conn = sqlite3.connect(DB_PATH, timeout=30)
conn.row_factory = sqlite3.Row
conn.execute("PRAGMA journal_mode=WAL")
conn.execute("PRAGMA busy_timeout=30000")
conn.execute("PRAGMA foreign_keys=ON")
return conn
def init_db():
conn = get_db()
conn.executescript("""
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT UNIQUE NOT NULL,
password_hash TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS domains (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT UNIQUE NOT NULL,
description TEXT,
status TEXT DEFAULT 'active',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
created_by INTEGER REFERENCES users(id)
);
CREATE TABLE IF NOT EXISTS certificates (
id INTEGER PRIMARY KEY AUTOINCREMENT,
domain_id INTEGER REFERENCES domains(id),
subject TEXT NOT NULL,
san TEXT,
serial TEXT UNIQUE,
status TEXT DEFAULT 'pending',
cert_path TEXT,
issued_at TIMESTAMP,
expires_at TIMESTAMP,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
created_by INTEGER REFERENCES users(id)
);
CREATE TABLE IF NOT EXISTS api_keys (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
key_hash TEXT UNIQUE NOT NULL,
prefix TEXT NOT NULL,
permissions TEXT DEFAULT 'read',
active BOOLEAN DEFAULT 1,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
created_by INTEGER REFERENCES users(id)
);
CREATE TABLE IF NOT EXISTS audit_log (
id INTEGER PRIMARY KEY AUTOINCREMENT,
action TEXT NOT NULL,
details TEXT,
user_id INTEGER REFERENCES users(id),
ip_address TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
""")
cur = conn.cursor()
cur.execute("SELECT id FROM users WHERE username = ?", (ADMIN_USERNAME,))
if not cur.fetchone():
pw_hash = bcrypt.hashpw(b"CHANGE_ME_ADMIN_PASS", bcrypt.gensalt())
if isinstance(pw_hash, bytes): pw_hash = pw_hash.decode()
cur.execute("INSERT INTO users (username, password_hash) VALUES (?, ?)",
(ADMIN_USERNAME, pw_hash))
conn.commit()
conn.close()
def hash_password(password):
h = bcrypt.hashpw(password.encode(), bcrypt.gensalt())
return h.decode() if isinstance(h, bytes) else h
def verify_password(password, hash_):
if isinstance(hash_, str): hash_ = hash_.encode()
return bcrypt.checkpw(password.encode(), hash_)

138
api/signing.py Normal file
View File

@ -0,0 +1,138 @@
import subprocess, datetime, os, hashlib, ipaddress, re
from cryptography import x509
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.x509.oid import NameOID
from config import *
TMP_DIR = "/var/lib/certauth/tmp"
os.makedirs(TMP_DIR, exist_ok=True)
def get_root_pub_key():
with open(YK_PUB_ROOT, "rb") as f:
return serialization.load_pem_public_key(f.read())
def get_int_pub_key():
with open(YK_PUB_INT, "rb") as f:
return serialization.load_pem_public_key(f.read())
def get_root_ca_cert():
with open(ROOT_CA_PATH, "rb") as f:
return x509.load_pem_x509_certificate(f.read())
def der_len(n):
if n < 0x80: return bytes([n])
elif n < 0x100: return bytes([0x81, n])
return bytes([0x82, n>>8, n&0xff])
def sign_tbs_with_yk(tbs_bytes, yk_pin, token_label="certauth Intermediate CA"):
tbs_file = os.path.join(TMP_DIR, "tbs_sign.der")
sig_file = os.path.join(TMP_DIR, "sig_out.bin")
with open(tbs_file, "wb") as f:
f.write(tbs_bytes)
r = subprocess.run([
"sudo", "pkcs11-tool", "--module", PKCS11_MODULE,
"--login", "--pin", yk_pin,
"--sign", "--mechanism", "ECDSA-SHA384",
"--token-label", token_label,
"--label", "SIGN key",
"--input-file", tbs_file,
"--output-file", sig_file
], capture_output=True, text=True)
if r.returncode != 0:
return None, r.stderr
with open(sig_file, "rb") as f:
raw = f.read()
rb = raw[:48].lstrip(b"\x00") or b"\x00"
sb = raw[48:].lstrip(b"\x00") or b"\x00"
if rb[0] & 0x80: rb = b"\x00" + rb
if sb[0] & 0x80: sb = b"\x00" + sb
r_der = b"\x02" + bytes([len(rb)]) + rb
s_der = b"\x02" + bytes([len(sb)]) + sb
seq = b"\x30" + bytes([len(r_der+s_der)]) + r_der + s_der
bs = b"\x00" + seq
return b"\x03" + bytes([len(bs)]) + bs, None
def build_leaf_cert(cn, sans, days=365):
root_cert = get_root_ca_cert()
int_pub = get_int_pub_key()
root_pub = get_root_pub_key()
leaf_key = ec.generate_private_key(ec.SECP384R1())
subject = x509.Name([
x509.NameAttribute(NameOID.COUNTRY_NAME, "US"),
x509.NameAttribute(NameOID.ORGANIZATION_NAME, "Home"),
x509.NameAttribute(NameOID.COMMON_NAME, cn),
])
issuer = x509.Name([
x509.NameAttribute(NameOID.COUNTRY_NAME, "US"),
x509.NameAttribute(NameOID.ORGANIZATION_NAME, "Home"),
x509.NameAttribute(NameOID.COMMON_NAME, "certauth Intermediate CA"),
])
builder = (x509.CertificateBuilder()
.subject_name(subject).issuer_name(issuer)
.public_key(leaf_key.public_key())
.serial_number(x509.random_serial_number())
.not_valid_before(datetime.datetime.now(datetime.timezone.utc))
.not_valid_after(datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(days=days))
.add_extension(x509.BasicConstraints(ca=False, path_length=None), critical=True)
.add_extension(x509.KeyUsage(
digital_signature=True, key_encipherment=True,
key_cert_sign=False, crl_sign=False,
content_commitment=False, data_encipherment=False,
key_agreement=False, encipher_only=False, decipher_only=False), critical=True)
.add_extension(x509.ExtendedKeyUsage([
x509.oid.ExtendedKeyUsageOID.SERVER_AUTH,
x509.oid.ExtendedKeyUsageOID.CLIENT_AUTH,
]), critical=False)
.add_extension(x509.SubjectKeyIdentifier.from_public_key(leaf_key.public_key()), critical=False)
.add_extension(x509.AuthorityKeyIdentifier.from_issuer_public_key(int_pub), critical=False))
if sans:
san_list = []
for s in sans.split(","):
s = s.strip()
# Check if it's an IP address
if re.match(r"^\d{1,3}(\.\d{1,3}){3}$", s):
san_list.append(x509.IPAddress(ipaddress.ip_address(s)))
else:
san_list.append(x509.DNSName(s))
builder = builder.add_extension(x509.SubjectAlternativeName(san_list), critical=False)
tmp = ec.generate_private_key(ec.SECP384R1())
temp = builder.sign(tmp, hashes.SHA384())
td = temp.public_bytes(serialization.Encoding.DER)
o = 1
if td[o] & 0x80: n = td[o] & 0x7f; o += 1 + n
else: o += 1
tbs_start = o
o += 1
if td[o] & 0x80: n = td[o] & 0x7f; tl = int.from_bytes(td[o+1:o+1+n], "big"); o += 1 + n
else: tl = td[o]; o += 1
tbs_end = o + tl
tbs_full = td[tbs_start:tbs_end]
alg_start = tbs_end
o2 = alg_start + 1
if td[o2] & 0x80: n = td[o2] & 0x7f; al = int.from_bytes(td[o2+1:o2+1+n], "big"); o2 += 1 + n
else: al = td[o2]; o2 += 1
alg_full = td[alg_start:o2+al]
new_sig, err = sign_tbs_with_yk(tbs_full, YK_INT_PIN)
if new_sig is None: return None, err
content = tbs_full + alg_full + new_sig
cl = len(content)
final = b"\x30\x82" + bytes([cl>>8, cl&0xff]) + content
der_file = os.path.join(TMP_DIR, "leaf.der")
pem_file = os.path.join(TMP_DIR, "leaf.pem")
with open(der_file, "wb") as f: f.write(final)
r = subprocess.run(["openssl", "x509", "-inform", "DER", "-outform", "PEM",
"-in", der_file, "-out", pem_file],
capture_output=True, text=True)
if r.returncode != 0: return None, r.stderr
with open(pem_file) as f: leaf_pem = f.read()
key_pem = leaf_key.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.PKCS8,
encryption_algorithm=serialization.NoEncryption()
).decode()
cert = x509.load_pem_x509_certificate(leaf_pem.encode())
serial = format(cert.serial_number, 'x')
return {"cert_pem": leaf_pem, "key_pem": key_pem, "serial": serial,
"expires_at": cert.not_valid_after.isoformat()}, None

30
api/templates/base.html Normal file
View File

@ -0,0 +1,30 @@
<!DOCTYPE html>
<html lang="en" class="bg-gray-900 text-gray-100">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>CertAuth{% block title %}{% endblock %}</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://unpkg.com/htmx.org@1.9.10"></script>
</head>
<body class="min-h-screen">
{% if user %}
<nav class="bg-gray-800 border-b border-gray-700 px-6 py-3 flex items-center justify-between">
<div class="flex items-center gap-6">
<a href="/" class="font-bold text-lg text-blue-400 hover:text-blue-300">CertAuth</a>
<a href="/domains" class="hover:text-blue-400">Domains</a>
<a href="/certs" class="hover:text-blue-400">Certificates</a>
<a href="/history" class="hover:text-blue-400">History</a>
<a href="/setup" class="hover:text-blue-400">Setup</a>
</div>
<div class="flex items-center gap-4">
<span class="text-sm text-gray-400">{{ user.get("sub", "") }}</span>
<a href="/logout" class="text-sm text-gray-400 hover:text-white">Logout</a>
</div>
</nav>
{% endif %}
<main class="p-6 max-w-6xl mx-auto">
{% block content %}{% endblock %}
</main>
</body>
</html>

67
api/templates/certs.html Normal file
View File

@ -0,0 +1,67 @@
{% extends "base.html" %}
{% block title %} - Certificates{% endblock %}
{% block content %}
<div class="flex items-center justify-between mb-6">
<h1 class="text-2xl font-bold">Certificates</h1>
</div>
<div class="bg-gray-800 rounded-lg p-6 border border-gray-700 mb-6">
<h2 class="font-bold mb-4">Request New Certificate</h2>
<form hx-post="/api/certs/web/request" hx-swap="innerHTML" hx-target="#cert-result"
class="flex gap-3 flex-wrap">
<input name="cn" placeholder="CN (e.g., git.example.com)" required
class="bg-gray-700 border border-gray-600 rounded px-3 py-2 text-white flex-1 min-w-[200px] focus:outline-none focus:border-blue-500">
<input name="sans" placeholder="SANs (comma-separated, e.g., git.example.com,*.git.example.com)"
class="bg-gray-700 border border-gray-600 rounded px-3 py-2 text-white flex-1 min-w-[200px] focus:outline-none focus:border-blue-500">
<select name="domain_id" class="bg-gray-700 border border-gray-600 rounded px-3 py-2 text-white focus:outline-none focus:border-blue-500">
<option value="0">No domain</option>
{% for d in domains %}
<option value="{{ d.id }}">{{ d.name }}</option>
{% endfor %}
</select>
<button type="submit" class="bg-blue-600 hover:bg-blue-500 px-4 py-2 rounded font-medium">Request</button>
</form>
<div id="cert-result" class="mt-3 text-sm"></div>
</div>
<div class="bg-gray-800 rounded-lg border border-gray-700 overflow-hidden">
<table class="w-full text-sm">
<thead class="border-b border-gray-700">
<tr>
<th class="text-left p-3 text-gray-400">Subject</th>
<th class="text-left p-3 text-gray-400">Domain</th>
<th class="text-left p-3 text-gray-400">Status</th>
<th class="text-left p-3 text-gray-400">Expires</th>
<th class="text-left p-3 text-gray-400">Actions</th>
</tr>
</thead>
<tbody>
{% for c in certs %}
<tr class="border-b border-gray-700/50">
<td class="p-3 font-mono">{{ c.subject }}</td>
<td class="p-3">{{ c.domain_name or '-' }}</td>
<td class="p-3">
<span class="px-2 py-1 rounded text-xs {% if c.status == 'issued' %}bg-green-900 text-green-300{% elif c.status == 'pending' %}bg-yellow-900 text-yellow-300{% endif %}">
{{ c.status }}
</span>
</td>
<td class="p-3 text-gray-400">{{ c.expires_at[:10] if c.expires_at else '-' }}</td>
<td class="p-3">
{% if c.status == 'issued' %}
<a href="/api/certs/{{ c.id }}/pem" class="text-blue-400 hover:text-blue-300 mr-2">PEM</a>
<a href="/api/certs/{{ c.id }}/pfx" class="text-blue-400 hover:text-blue-300">PFX</a>
{% elif c.status == 'pending' %}
<button hx-post="/api/certs/{{ c.id }}/sign/web" hx-target="#sign-msg-{{ c.id }}"
class="text-yellow-400 hover:text-yellow-300">Issue</button>
<span id="sign-msg-{{ c.id }}" class="ml-2"></span>
{% endif %}
</td>
</tr>
{% endfor %}
{% if not certs %}
<tr><td colspan="5" class="p-6 text-center text-gray-500">No certificates yet</td></tr>
{% endif %}
</tbody>
</table>
</div>
{% endblock %}

View File

@ -0,0 +1,62 @@
{% extends "base.html" %}
{% block title %} - Dashboard{% endblock %}
{% block content %}
<h1 class="text-2xl font-bold mb-6">Dashboard</h1>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-8">
<div class="bg-gray-800 rounded-lg p-6 border border-gray-700">
<div class="text-sm text-gray-400">Issued Certificates</div>
<div class="text-3xl font-bold text-green-400">{{ issued }}</div>
</div>
<div class="bg-gray-800 rounded-lg p-6 border border-gray-700">
<div class="text-sm text-gray-400">Pending Issue</div>
<div class="text-3xl font-bold text-yellow-400">{{ pending }}</div>
</div>
<div class="bg-gray-800 rounded-lg p-6 border border-gray-700">
<div class="text-sm text-gray-400">Registered Domains</div>
<div class="text-3xl font-bold text-blue-400">{{ domains|length }}</div>
</div>
</div>
<h2 class="text-xl font-bold mb-4">Recent Certificates</h2>
<div class="bg-gray-800 rounded-lg border border-gray-700 overflow-hidden">
<table class="w-full text-sm">
<thead class="bg-gray-750 border-b border-gray-700">
<tr>
<th class="text-left p-3 text-gray-400">Subject</th>
<th class="text-left p-3 text-gray-400">Domain</th>
<th class="text-left p-3 text-gray-400">Status</th>
<th class="text-left p-3 text-gray-400">Expires</th>
<th class="text-left p-3 text-gray-400">Actions</th>
</tr>
</thead>
<tbody>
{% for c in certs %}
<tr class="border-b border-gray-700/50">
<td class="p-3 font-mono">{{ c.subject }}</td>
<td class="p-3">{{ c.domain_name or '-' }}</td>
<td class="p-3">
<span class="px-2 py-1 rounded text-xs {% if c.status == 'issued' %}bg-green-900 text-green-300{% elif c.status == 'pending' %}bg-yellow-900 text-yellow-300{% else %}bg-gray-700{% endif %}">
{{ c.status }}
</span>
</td>
<td class="p-3 text-gray-400">{{ c.expires_at[:10] if c.expires_at else '-' }}</td>
<td class="p-3">
{% if c.status == 'issued' %}
<a href="/api/certs/{{ c.id }}/pem" class="text-blue-400 hover:text-blue-300 mr-2">PEM</a>
<a href="/api/certs/{{ c.id }}/pfx" class="text-blue-400 hover:text-blue-300">PFX</a>
{% elif c.status == 'pending' %}
<button hx-post="/api/certs/{{ c.id }}/sign/web" hx-target="#sign-msg-{{ c.id }}"
class="text-yellow-400 hover:text-yellow-300">Issue</button>
<span id="sign-msg-{{ c.id }}" class="ml-2"></span>
{% endif %}
</td>
</tr>
{% endfor %}
{% if not certs %}
<tr><td colspan="5" class="p-6 text-center text-gray-500">No certificates yet</td></tr>
{% endif %}
</tbody>
</table>
</div>
{% endblock %}

View File

@ -0,0 +1,46 @@
{% extends "base.html" %}
{% block title %} - Domains{% endblock %}
{% block content %}
<div class="flex items-center justify-between mb-6">
<h1 class="text-2xl font-bold">Domains</h1>
</div>
<div class="bg-gray-800 rounded-lg p-6 border border-gray-700 mb-6">
<h2 class="font-bold mb-4">Register New Domain</h2>
<form hx-post="/api/domains/web" hx-swap="innerHTML" hx-target="#domain-result"
class="flex gap-3 flex-wrap">
<input name="name" placeholder="*.example.com" required
class="bg-gray-700 border border-gray-600 rounded px-3 py-2 text-white flex-1 min-w-[200px] focus:outline-none focus:border-blue-500">
<input name="description" placeholder="Description (optional)"
class="bg-gray-700 border border-gray-600 rounded px-3 py-2 text-white flex-1 min-w-[200px] focus:outline-none focus:border-blue-500">
<button type="submit" class="bg-blue-600 hover:bg-blue-500 px-4 py-2 rounded font-medium">Register</button>
</form>
<div id="domain-result" class="mt-3 text-sm"></div>
</div>
<div class="bg-gray-800 rounded-lg border border-gray-700 overflow-hidden">
<table class="w-full text-sm">
<thead class="border-b border-gray-700">
<tr>
<th class="text-left p-3 text-gray-400">Domain</th>
<th class="text-left p-3 text-gray-400">Description</th>
<th class="text-left p-3 text-gray-400">Status</th>
<th class="text-left p-3 text-gray-400">Created</th>
</tr>
</thead>
<tbody>
{% for d in domains %}
<tr class="border-b border-gray-700/50">
<td class="p-3 font-mono">{{ d.name }}</td>
<td class="p-3 text-gray-400">{{ d.description or '-' }}</td>
<td class="p-3"><span class="px-2 py-1 rounded text-xs bg-green-900 text-green-300">{{ d.status }}</span></td>
<td class="p-3 text-gray-400">{{ d.created_at[:10] }}</td>
</tr>
{% endfor %}
{% if not domains %}
<tr><td colspan="4" class="p-6 text-center text-gray-500">No domains registered</td></tr>
{% endif %}
</tbody>
</table>
</div>
{% endblock %}

View File

@ -0,0 +1,54 @@
{% extends "base.html" %}
{% block title %} - History{% endblock %}
{% block content %}
<div class="flex items-center justify-between mb-6">
<h1 class="text-2xl font-bold">Certificate History</h1>
</div>
<div class="bg-gray-800 rounded-lg border border-gray-700 overflow-hidden">
<table class="w-full text-sm">
<thead class="border-b border-gray-700">
<tr>
<th class="text-left p-3 text-gray-400">Serial</th>
<th class="text-left p-3 text-gray-400">Subject</th>
<th class="text-left p-3 text-gray-400">Domain</th>
<th class="text-left p-3 text-gray-400">SANs</th>
<th class="text-left p-3 text-gray-400">Status</th>
<th class="text-left p-3 text-gray-400">Issued</th>
<th class="text-left p-3 text-gray-400">Expires</th>
<th class="text-left p-3 text-gray-400">Actions</th>
</tr>
</thead>
<tbody>
{% for c in certs %}
<tr class="border-b border-gray-700/50">
<td class="p-3 font-mono text-xs">{{ c.serial or '-' }}</td>
<td class="p-3 font-mono">{{ c.subject }}</td>
<td class="p-3">{{ c.domain_name or '-' }}</td>
<td class="p-3 text-xs text-gray-400">{{ c.san or '-' }}</td>
<td class="p-3">
<span class="px-2 py-1 rounded text-xs {% if c.status == 'issued' %}bg-green-900 text-green-300{% elif c.status == 'pending' %}bg-yellow-900 text-yellow-300{% else %}bg-gray-700{% endif %}">
{{ c.status }}
</span>
</td>
<td class="p-3 text-gray-400">{{ c.issued_at[:10] if c.issued_at else '-' }}</td>
<td class="p-3 text-gray-400">{{ c.expires_at[:10] if c.expires_at else '-' }}</td>
<td class="p-3">
{% if c.status == 'issued' %}
<a href="/api/certs/{{ c.id }}/pem" class="text-blue-400 hover:text-blue-300 mr-2">PEM</a>
<a href="/api/certs/{{ c.id }}/pfx" class="text-blue-400 hover:text-blue-300">PFX</a>
{% elif c.status == 'pending' %}
<button hx-post="/api/certs/{{ c.id }}/sign/web" hx-target="#sign-msg-{{ c.id }}"
class="text-yellow-400 hover:text-yellow-300">Issue</button>
<span id="sign-msg-{{ c.id }}" class="ml-2"></span>
{% endif %}
</td>
</tr>
{% endfor %}
{% if not certs %}
<tr><td colspan="8" class="p-6 text-center text-gray-500">No certificates yet</td></tr>
{% endif %}
</tbody>
</table>
</div>
{% endblock %}

34
api/templates/login.html Normal file
View File

@ -0,0 +1,34 @@
<!DOCTYPE html>
<html lang="en" class="bg-gray-900 text-gray-100">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>CertAuth - Login</title>
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body class="min-h-screen flex items-center justify-center">
<div class="bg-gray-800 rounded-lg p-8 border border-gray-700 w-full max-w-md">
<h1 class="text-2xl font-bold mb-2 text-blue-400">CertAuth</h1>
<p class="text-gray-400 mb-6">Certificate Authority Management</p>
{% if error %}
<div class="bg-red-900/50 border border-red-700 rounded p-3 mb-4 text-red-300 text-sm">{{ error }}</div>
{% endif %}
<form method="post" action="/login">
<div class="mb-4">
<label class="block text-sm text-gray-400 mb-1">Username</label>
<input type="text" name="username" required
class="w-full bg-gray-700 border border-gray-600 rounded px-3 py-2 text-white focus:outline-none focus:border-blue-500">
</div>
<div class="mb-6">
<label class="block text-sm text-gray-400 mb-1">Password</label>
<input type="password" name="password" required
class="w-full bg-gray-700 border border-gray-600 rounded px-3 py-2 text-white focus:outline-none focus:border-blue-500">
</div>
<button type="submit"
class="w-full bg-blue-600 hover:bg-blue-500 text-white font-medium py-2 rounded transition">
Sign In
</button>
</form>
</div>
</body>
</html>

88
api/templates/setup.html Normal file
View File

@ -0,0 +1,88 @@
{% extends "base.html" %}
{% block title %} - Setup{% endblock %}
{% block content %}
<h1 class="text-2xl font-bold mb-2">Setup</h1>
<p class="text-gray-400 mb-6">Install the CA chain on client machines to trust certificates from this authority.</p>
<!-- Quick Install -->
<div class="bg-blue-900/50 rounded-lg p-6 border border-blue-700 mb-6">
<h2 class="font-bold mb-3">Quick Install</h2>
<p class="text-sm text-gray-300 mb-3">Run one command on any machine to download and install the CA chain automatically.</p>
<div class="space-y-3">
<div>
<span class="text-sm text-gray-400">Linux / macOS</span>
<pre class="bg-gray-900 rounded p-3 text-sm mt-1 overflow-x-auto"><code>curl -sL http://192.168.8.248/setup.sh | sudo bash</code></pre>
</div>
<div>
<span class="text-sm text-gray-400">Windows (PowerShell)</span>
<pre class="bg-gray-900 rounded p-3 text-sm mt-1 overflow-x-auto"><code>iwr http://192.168.8.248/setup.ps1 -UseBasicParsing | iex</code></pre>
</div>
</div>
</div>
<!-- Download CA Chain -->
<div class="bg-gray-800 rounded-lg p-6 border border-gray-700 mb-6">
<h2 class="font-bold mb-4">Manual Download</h2>
<p class="text-sm text-gray-400 mb-4">Contains the Intermediate + Root CA certificates.</p>
<a href="/api/ca-chain" class="inline-block bg-blue-600 hover:bg-blue-500 px-4 py-2 rounded font-medium text-white">Download ca-chain.crt</a>
</div>
<!-- Platform Instructions -->
<div class="space-y-4">
<h2 class="font-bold text-lg">Manual Installation</h2>
<!-- Linux Debian/Ubuntu -->
<div class="bg-gray-800 rounded-lg p-6 border border-gray-700">
<h3 class="font-bold mb-2">Linux (Debian/Ubuntu)</h3>
<pre class="bg-gray-900 rounded p-3 text-sm overflow-x-auto"><code>sudo cp ca-chain.crt /usr/local/share/ca-certificates/certauth.crt
sudo update-ca-certificates</code></pre>
</div>
<!-- Linux Fedora/RHEL -->
<div class="bg-gray-800 rounded-lg p-6 border border-gray-700">
<h3 class="font-bold mb-2">Linux (Fedora/RHEL)</h3>
<pre class="bg-gray-900 rounded p-3 text-sm overflow-x-auto"><code># System trust store (curl, openssl)
sudo cp ca-chain.crt /etc/pki/ca-trust/source/anchors/certauth.crt
sudo update-ca-trust
# NSS database (Firefox, Thunderbird)
sudo certutil -A -n "CertAuth Root CA" -t "CT,Cu,Tu" -d sql:/etc/pki/nssdb/ -i ca-chain.crt</code></pre>
</div>
<!-- LibreWolf -->
<div class="bg-gray-800 rounded-lg p-6 border border-gray-700">
<h3 class="font-bold mb-2">LibreWolf</h3>
<p class="text-sm text-gray-400 mb-2">LibreWolf uses its own NSS database and disables enterprise roots by default. Enable enterprise roots in <code class="bg-gray-700 px-1 rounded">about:config</code> → set <code class="bg-gray-700 px-1 rounded">security.enterprise_roots.enabled</code> to <code class="bg-gray-700 px-1 rounded">true</code>.</p>
<pre class="bg-gray-900 rounded p-3 text-sm overflow-x-auto"><code># Import root CA to your LibreWolf profile
certutil -A -n "CertAuth Root CA" -t "CT,Cu,Tu" -d ~/.librewolf/&lt;profile&gt;/ -i ca-chain.crt
# Clear SSL state cache if you previously got a cert error
rm ~/.librewolf/&lt;profile&gt;/SiteSecurityServiceState.bin</code></pre>
</div>
<!-- macOS -->
<div class="bg-gray-800 rounded-lg p-6 border border-gray-700">
<h3 class="font-bold mb-2">macOS</h3>
<pre class="bg-gray-900 rounded p-3 text-sm overflow-x-auto"><code>sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain ca-chain.crt</code></pre>
</div>
<!-- Windows -->
<div class="bg-gray-800 rounded-lg p-6 border border-gray-700">
<h3 class="font-bold mb-2">Windows</h3>
<p class="text-sm text-gray-400 mb-2">Double-click <code class="bg-gray-700 px-1 rounded">ca-chain.crt</code>, then:</p>
<pre class="bg-gray-900 rounded p-3 text-sm overflow-x-auto"><code>1. Click "Install Certificate"
2. Select "Local Machine" → Next
3. Select "Place all certificates in the following store"
4. Browse → "Trusted Root Certification Authorities"
5. OK → Next → Finish</code></pre>
</div>
<!-- Docker -->
<div class="bg-gray-800 rounded-lg p-6 border border-gray-700">
<h3 class="font-bold mb-2">Docker</h3>
<pre class="bg-gray-900 rounded p-3 text-sm overflow-x-auto"><code>COPY ca-chain.crt /usr/local/share/ca-certificates/certauth.crt
RUN update-ca-certificates</code></pre>
</div>
</div>
{% endblock %}

16
certauth-api.service Normal file
View File

@ -0,0 +1,16 @@
[Unit]
Description=CertAuth API
After=network.target pcscd.service
[Service]
Type=simple
User=certauth
Group=certauth
WorkingDirectory=/opt/certauth/api
Environment=PYTHONUNBUFFERED=1
ExecStart=/usr/bin/python3 -m uvicorn main:app --host 127.0.0.1 --port 8000
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target

38
landing/index.html Normal file
View File

@ -0,0 +1,38 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Home Network</title>
<style>
:root { --bg: #0f172a; --card: #1e293b; --text: #f8fafc; --accent: #38bdf8; }
body { font-family: system-ui, sans-serif; background: var(--bg); color: var(--text); margin: 0; padding: 2rem; }
.container { max-width: 1200px; margin: 0 auto; }
h1 { text-align: center; margin-bottom: 3rem; font-size: 2.5rem; background: linear-gradient(135deg, var(--accent), #818cf8); -webkit-background-clip: text; -webkit-text-fill-color: transparent; }
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 1.5rem; }
.card { background: var(--card); border-radius: 12px; padding: 1.5rem; text-decoration: none; color: var(--text); transition: all 0.2s; border: 1px solid rgba(255,255,255,0.1); }
.card:hover { transform: translateY(-2px); border-color: var(--accent); box-shadow: 0 4px 12px rgba(56, 189, 248, 0.1); }
.card h2 { margin: 0 0 0.5rem; font-size: 1.25rem; }
.card p { margin: 0; color: #94a3b8; font-size: 0.9rem; }
.icon { font-size: 2rem; margin-bottom: 1rem; }
</style>
</head>
<body>
<div class="container">
<h1>Home Network</h1>
<div class="grid">
<a href="https://tv.example.com" class="card"><div class="icon">🎬</div><h2>TV & Media</h2><p>Jellyfin media server</p></a>
<a href="https://ai.example.com" class="card"><div class="icon">🤖</div><h2>AI Chat</h2><p>Open WebUI</p></a>
<a href="https://agents.example.com" class="card"><div class="icon"></div><h2>Automation</h2><p>n8n workflows</p></a>
<a href="https://papers.example.com" class="card"><div class="icon">📄</div><h2>Research Papers</h2><p>Arxiv browser</p></a>
<a href="https://pin.example.com" class="card"><div class="icon">📌</div><h2>Bookmarks</h2><p>Karakeep</p></a>
<a href="https://clip.example.com" class="card"><div class="icon">🎞️</div><h2>Clips</h2><p>Jelly Clip UI</p></a>
<a href="https://paste.example.com" class="card"><div class="icon">📋</div><h2>PasteBin</h2><p>Text sharing</p></a>
<a href="https://llm.example.com" class="card"><div class="icon">🧠</div><h2>LLM Proxy</h2><p>LiteLLM</p></a>
<a href="https://monitor.example.com" class="card"><div class="icon">📊</div><h2>Monitoring</h2><p>Grafana</p></a>
<a href="https://stocks.example.com" class="card"><div class="icon">📈</div><h2>Stock Docs</h2><p>Financial docs</p></a>
<a href="https://stockmcp.example.com" class="card"><div class="icon">💹</div><h2>Stock MCP</h2><p>Market data API</p></a>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,34 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Playground</title>
<style>
:root { --bg: #0f172a; --card: #1e293b; --text: #f8fafc; --accent: #38bdf8; }
body { font-family: system-ui, sans-serif; background: var(--bg); color: var(--text); margin: 0; padding: 2rem; }
.container { max-width: 1200px; margin: 0 auto; }
h1 { text-align: center; margin-bottom: 3rem; font-size: 2.5rem; background: linear-gradient(135deg, var(--accent), #818cf8); -webkit-background-clip: text; -webkit-text-fill-color: transparent; }
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 1.5rem; }
.card { background: var(--card); border-radius: 12px; padding: 1.5rem; text-decoration: none; color: var(--text); transition: all 0.2s; border: 1px solid rgba(255,255,255,0.1); }
.card:hover { transform: translateY(-2px); border-color: var(--accent); box-shadow: 0 4px 12px rgba(56, 189, 248, 0.1); }
.card h2 { margin: 0 0 0.5rem; font-size: 1.25rem; }
.card p { margin: 0; color: #94a3b8; font-size: 0.9rem; }
.icon { font-size: 2rem; margin-bottom: 1rem; }
</style>
</head>
<body>
<div class="container">
<h1>Playground</h1>
<div class="grid">
<a href="https://pinvault.example.com" class="card"><div class="icon">🔐</div><h2>PinVault</h2><p>Password manager</p></a>
<a href="https://youtube.example.com" class="card"><div class="icon">📺</div><h2>YouTube</h2><p>YouTube CLI</p></a>
<a href="https://archive.example.com" class="card"><div class="icon">🏛️</div><h2>Archive</h2><p>Website archiving</p></a>
<a href="https://vote.example.com" class="card"><div class="icon">🗳️</div><h2>Vote</h2><p>Voting app</p></a>
<a href="https://search.example.com" class="card"><div class="icon">🔍</div><h2>Search</h2><p>Privacy metasearch</p></a>
<a href="https://paste.example.com" class="card"><div class="icon">📋</div><h2>Paste</h2><p>Pastebin service</p></a>
<a href="https://ai.example.com" class="card"><div class="icon">🤖</div><h2>AI</h2><p>Local LLM</p></a>
</div>
</div>
</body>
</html>

744
nginx-local.conf Normal file
View File

@ -0,0 +1,744 @@
# ============================================================
# example.com - Main landing page (HTTP only, no cert for bare domain)
# ============================================================
server {
listen 80;
server_name example.com;
root /var/www/example.com;
index index.html index.htm;
location / {
try_files $uri $uri/ =404;
}
}
# ============================================================
# web.example.com - HTTPS landing page
# ============================================================
server {
listen 443 ssl;
server_name web.example.com www.web.example.com;
ssl_certificate /etc/ssl/certs/web.example.com.pem;
ssl_certificate_key /etc/ssl/private/web.example.com.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
root /var/www/example.com;
index index.html index.htm;
location / {
try_files $uri $uri/ =404;
}
}
# HTTP -> HTTPS redirect for web.example.com
server {
listen 80;
server_name web.example.com www.web.example.com;
return 301 https://$host$request_uri;
}
# ============================================================
# tv.example.com - Jellyfin
# ============================================================
server {
listen 443 ssl;
server_name tv.example.com;
ssl_certificate /etc/ssl/certs/tv.example.com.pem;
ssl_certificate_key /etc/ssl/private/tv.example.com.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
location / {
proxy_pass http://localhost:8096;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
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;
}
}
server {
listen 80;
server_name tv.example.com;
return 301 https://$host$request_uri;
}
# ============================================================
# ai.example.com - Open WebUI
# ============================================================
server {
listen 443 ssl;
server_name ai.example.com;
ssl_certificate /etc/ssl/certs/ai.example.com.pem;
ssl_certificate_key /etc/ssl/private/ai.example.com.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
location / {
proxy_pass http://localhost:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
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;
proxy_buffering off;
proxy_read_timeout 600s;
proxy_send_timeout 600s;
client_max_body_size 20m;
gzip off;
}
}
server {
listen 80;
server_name ai.example.com;
return 301 https://$host$request_uri;
}
# ============================================================
# agents.example.com - n8n
# ============================================================
server {
listen 443 ssl;
server_name agents.example.com;
ssl_certificate /etc/ssl/certs/agents.example.com.pem;
ssl_certificate_key /etc/ssl/private/agents.example.com.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
location / {
proxy_pass http://localhost:5678;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
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;
proxy_read_timeout 600s;
proxy_send_timeout 600s;
gzip off;
}
}
server {
listen 80;
server_name agents.example.com;
return 301 https://$host$request_uri;
}
# ============================================================
# papers.example.com - Arxiv Sanity Lite
# ============================================================
server {
listen 443 ssl;
server_name papers.example.com;
ssl_certificate /etc/ssl/certs/papers.example.com.pem;
ssl_certificate_key /etc/ssl/private/papers.example.com.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
location / {
proxy_pass http://localhost:7800;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
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;
}
}
server {
listen 80;
server_name papers.example.com;
return 301 https://$host$request_uri;
}
# ============================================================
# pin.example.com - Karakeep
# ============================================================
server {
listen 443 ssl;
server_name pin.example.com;
ssl_certificate /etc/ssl/certs/pin.example.com.pem;
ssl_certificate_key /etc/ssl/private/pin.example.com.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
location / {
proxy_pass http://localhost:4444;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
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;
}
}
server {
listen 80;
server_name pin.example.com;
return 301 https://$host$request_uri;
}
# ============================================================
# clip.example.com - Jelly Clip
# ============================================================
server {
listen 443 ssl;
server_name clip.example.com;
ssl_certificate /etc/ssl/certs/clip.example.com.pem;
ssl_certificate_key /etc/ssl/private/clip.example.com.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
location / {
proxy_pass http://localhost:5001;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
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;
}
}
server {
listen 80;
server_name clip.example.com;
return 301 https://$host$request_uri;
}
# ============================================================
# paste.example.com - Pastebin
# ============================================================
server {
listen 443 ssl;
server_name paste.example.com;
ssl_certificate /etc/ssl/certs/paste.example.com.pem;
ssl_certificate_key /etc/ssl/private/paste.example.com.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
location / {
proxy_pass http://localhost:9780;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
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;
}
}
server {
listen 80;
server_name paste.example.com;
return 301 https://$host$request_uri;
}
# ============================================================
# llm.example.com - LiteLLM Proxy
# ============================================================
server {
listen 443 ssl;
server_name llm.example.com;
ssl_certificate /etc/ssl/certs/llm.example.com.pem;
ssl_certificate_key /etc/ssl/private/llm.example.com.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
location / {
proxy_pass http://localhost:4000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
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;
}
}
server {
listen 80;
server_name llm.example.com;
return 301 https://$host$request_uri;
}
# ============================================================
# monitor.example.com - Grafana
# ============================================================
server {
listen 443 ssl;
server_name monitor.example.com;
ssl_certificate /etc/ssl/certs/monitor.example.com.pem;
ssl_certificate_key /etc/ssl/private/monitor.example.com.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
location / {
proxy_pass http://localhost:3003;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
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;
}
}
server {
listen 80;
server_name monitor.example.com;
return 301 https://$host$request_uri;
}
# ============================================================
# stocks.example.com - Stock Docs Article Server
# ============================================================
server {
listen 443 ssl;
server_name stocks.example.com;
ssl_certificate /etc/ssl/certs/stocks.example.com.pem;
ssl_certificate_key /etc/ssl/private/stocks.example.com.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
location / {
proxy_pass http://localhost:5008;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
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;
}
}
server {
listen 80;
server_name stocks.example.com;
return 301 https://$host$request_uri;
}
# ============================================================
# stockmcp.example.com - Stock MCP
# ============================================================
server {
listen 443 ssl;
server_name stockmcp.example.com;
ssl_certificate /etc/ssl/certs/stockmcp.example.com.pem;
ssl_certificate_key /etc/ssl/private/stockmcp.example.com.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
location / {
proxy_pass http://localhost:5005;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
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;
}
}
server {
listen 80;
server_name stockmcp.example.com;
return 301 https://$host$request_uri;
}
# ============================================================
# play.example.com - (placeholder - specify backend port)
# ============================================================
server {
listen 443 ssl;
server_name play.example.com;
ssl_certificate /etc/ssl/certs/play.example.com.pem;
ssl_certificate_key /etc/ssl/private/play.example.com.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
location / {
return 503 "Service not configured - set backend port";
}
}
server {
listen 80;
server_name play.example.com;
return 301 https://$host$request_uri;
}
# ============================================================
# git.example.com - Gitea (HTTPS)
# ============================================================
server {
listen 443 ssl;
server_name git.example.com;
ssl_certificate /etc/ssl/certs/git.example.com.pem;
ssl_certificate_key /etc/ssl/private/git.example.com.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
client_max_body_size 50M;
location / {
proxy_pass http://192.168.8.192:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
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;
}
}
server {
listen 80;
server_name git.example.com;
return 301 https://$host$request_uri;
}
# books.example.com - Bookworm
server {
listen 80;
server_name books.example.com;
location / {
proxy_pass http://localhost:1333;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
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;
}
}
# ai-host.example.com - Ollama
server {
listen 80;
server_name ai-host.example.com;
location / {
proxy_pass http://192.168.8.124:11434;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
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;
}
}
# chat.example.com - Element/Matrix UI
server {
listen 80;
server_name chat.example.com;
location / {
proxy_pass http://localhost:8009;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
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;
}
}
# conduit.example.com - Matrix Server
server {
listen 80;
server_name conduit.example.com;
location / {
proxy_pass http://localhost:8448;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
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;
}
}
# tiktok.example.com
server {
listen 80;
server_name tiktok.example.com;
location / {
proxy_pass http://0.0.0.0:4321;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
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;
}
}
# archive.example.com - ArchiveBox
server {
listen 80;
server_name archive.example.com;
location / {
proxy_pass http://localhost:1234;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
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;
}
}
# loop.example.com - Karakeep Chrome extension
server {
listen 80;
server_name loop.example.com;
location / {
proxy_pass http://localhost:3010;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
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;
}
}
# board.example.com - Karakeep board
server {
listen 80;
server_name board.example.com;
location / {
proxy_pass http://localhost:3001;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
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;
}
}
# substack.example.com - Static Substack pages
server {
listen 80;
listen [::]:80;
server_name substack.example.com;
root /mnt/centralstoragemedia/Websites/Substack/substack_html_pages;
index index.html;
location /substack_html_pages/ {
alias /mnt/centralstoragemedia/Websites/Substack/substack_html_pages/;
try_files $uri $uri/ =404;
}
location /economics/ {
alias /mnt/centralstoragemedia/Websites/Substack/substack_html_pages/paulkrugman/;
try_files $uri $uri/ =404;
}
server_tokens off;
autoindex off;
location = /ai {
rewrite ^ /natesnewsletter.html break;
}
location = /economics {
rewrite ^ /paulkrugman.html break;
}
location = /games {
rewrite ^ /theshortcut.html break;
}
error_page 404 /404.html;
location = /404.html {
internal;
root /etc/nginx/html;
}
location ^~ /assets/js/ {
alias /mnt/centralstoragemedia/Websites/Substack/assets/js/;
default_type application/javascript;
expires 30d;
}
location ^~ /assets/css/ {
alias /mnt/centralstoragemedia/Websites/Substack/assets/css/;
default_type text/css;
expires 30d;
}
access_log /var/log/nginx/substack.access.log;
error_log /var/log/nginx/substack.error.log;
}
# kiwix.example.com - Wikipedia/Kiwix
server {
listen 80;
server_name kiwix.example.com;
location / {
proxy_pass http://127.0.0.1:8008;
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;
}
}
# archive2.example.com - ArchiveBox snapshots
server {
listen 80;
listen [::]:80;
server_name archive2.example.com;
root /mnt/centralstoragemedia/Websites/ArchiveBox/data/archive;
index singlefile.html;
location / {
try_files $uri/singlefile.html $uri/index.html $uri/output.pdf =404;
}
location ~ ^/([^/]+)/pdf$ {
try_files $uri /$1/output.pdf =404;
}
location ~ ^/([^/]+)/html$ {
try_files $uri /$1/output.html =404;
}
location ~ ^/([^/]+)/singlefile$ {
try_files $uri /$1/singlefile.html =404;
}
server_tokens off;
autoindex off;
error_page 404 /404.html;
location = /404.html {
internal;
root /etc/nginx/html;
}
access_log /var/log/nginx/archive2.access.log;
error_log /var/log/nginx/archive2.error.log;
}
# red.example.com - Reddit
server {
listen 80;
server_name red.example.com;
location / {
proxy_pass http://localhost:6006;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
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;
}
}
# space.example.com - NASA
server {
listen 80;
server_name space.example.com;
location / {
proxy_pass http://localhost:6008;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
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;
}
}
# insights.example.com
server {
listen 80;
server_name insights.example.com;
location / {
proxy_pass http://localhost:5010;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
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;
}
}
# podcast.example.com - Static podcast files
server {
listen 80;
server_name podcast.example.com;
root /mnt/centralstoragemedia/Podcasts;
location / {
try_files $uri.wav =404;
}
location ~* \.wav$ {
try_files $uri =404;
}
}

1746
setup-certauth.sh Normal file

File diff suppressed because it is too large Load Diff