feat: public SSO — fhirworx.io domain, Gitea OAuth2, Cloudflare tunnel
Migrate from homelab.fhirworx.io (LAN-only, IP allowlist) to fhirworx.io (public, Gitea SSO via oauth2-proxy). - Domain: homelab.fhirworx.io → fhirworx.io across all configs - SSO: oauth2-proxy (OIDC/Gitea) + auth-handler nginx for Traefik ForwardAuth (converts 401 → 302 redirect, same as corwins.media auth_request pattern) - Cloudflared: tunnel remote config with 20 hostnames → traefik, DNS CNAME records via CF API - Bootstrap: `docker compose run --rm wire` — idempotent cold-start that creates Gitea admin, OAuth2 app, oauth2-proxy credentials, clears Cloudflare Access apps, syncs tunnel config + DNS - Dashboard: rebranded FHIRWORX, HTTPS links, API tile added - Grafana/Woodpecker/Gitea ROOT_URLs updated to HTTPS
This commit is contained in:
@@ -37,7 +37,7 @@ DATABRICKS_TOKEN=
|
||||
# ── Deployment ──────────────────────────────────────
|
||||
# Base domain — all services get {name}.DOMAIN FQDNs.
|
||||
# DNS must resolve *.DOMAIN to HOST_IP.
|
||||
DOMAIN=homelab.fhirworx.io
|
||||
DOMAIN=fhirworx.io
|
||||
HOST_IP=192.168.1.192
|
||||
DOCKER_SOCK=/run/user/1000/docker.sock
|
||||
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,6 +1,9 @@
|
||||
# Virtual environment
|
||||
.venv/
|
||||
|
||||
# Bootstrap state (credentials, OAuth2 tokens)
|
||||
.state/
|
||||
|
||||
# Data directories (user data, not tracked)
|
||||
data/
|
||||
data/zotero/data/
|
||||
|
||||
34
README.md.j2
34
README.md.j2
@@ -14,7 +14,7 @@ Healthcare analytics platform on self-hosted infrastructure. Replaces dbt SQL mo
|
||||
# 1. Generate root key and seed .env
|
||||
ROOT_KEY=$(openssl rand -hex 32)
|
||||
cat > .env <<EOF
|
||||
DOMAIN=homelab.fhirworx.io
|
||||
DOMAIN=fhirworx.io
|
||||
HOST_IP=192.168.1.192
|
||||
EOF
|
||||
|
||||
@@ -45,24 +45,24 @@ pip install stack[aco,aws] # ACO analytics with AWS storage
|
||||
|
||||
## Services
|
||||
|
||||
All services route through Traefik at `*.homelab.fhirworx.io`.
|
||||
All services route through Traefik at `*.fhirworx.io`.
|
||||
|
||||
| Service | URL | Purpose |
|
||||
|---------|-----|---------|
|
||||
| API | `api` (internal) | FastAPI — health, pipelines, bib endpoints |
|
||||
| Notebooks | `notebooks.homelab.fhirworx.io` | Marimo notebooks (GPU-accelerated) |
|
||||
| Docs | `docs.homelab.fhirworx.io` | Docusaurus — API reference + CMS bibliography |
|
||||
| Gitea | `gitea.homelab.fhirworx.io` | Git server, container registry, LFS |
|
||||
| Zotero | `zotero.homelab.fhirworx.io` | Reference manager (KasmVNC desktop) |
|
||||
| Dashboard | `homelab.fhirworx.io` | Service launcher |
|
||||
| RustFS | `s3.homelab.fhirworx.io` | S3-compatible object storage |
|
||||
| Nessie | `nessie.homelab.fhirworx.io` | Git-like Iceberg catalog |
|
||||
| Polaris | `polaris.homelab.fhirworx.io` | Iceberg catalog with governance |
|
||||
| Trino | `trino.homelab.fhirworx.io` | Distributed SQL engine |
|
||||
| Grafana | `grafana.homelab.fhirworx.io` | Dashboards |
|
||||
| Prometheus | `prometheus.homelab.fhirworx.io` | Metrics |
|
||||
| Jaeger | `jaeger.homelab.fhirworx.io` | Distributed tracing |
|
||||
| Loki | `loki.homelab.fhirworx.io` | Log aggregation |
|
||||
| Notebooks | `notebooks.fhirworx.io` | Marimo notebooks (GPU-accelerated) |
|
||||
| Docs | `docs.fhirworx.io` | Docusaurus — API reference + CMS bibliography |
|
||||
| Gitea | `gitea.fhirworx.io` | Git server, container registry, LFS |
|
||||
| Zotero | `zotero.fhirworx.io` | Reference manager (KasmVNC desktop) |
|
||||
| Dashboard | `fhirworx.io` | Service launcher |
|
||||
| RustFS | `s3.fhirworx.io` | S3-compatible object storage |
|
||||
| Nessie | `nessie.fhirworx.io` | Git-like Iceberg catalog |
|
||||
| Polaris | `polaris.fhirworx.io` | Iceberg catalog with governance |
|
||||
| Trino | `trino.fhirworx.io` | Distributed SQL engine |
|
||||
| Grafana | `grafana.fhirworx.io` | Dashboards |
|
||||
| Prometheus | `prometheus.fhirworx.io` | Metrics |
|
||||
| Jaeger | `jaeger.fhirworx.io` | Distributed tracing |
|
||||
| Loki | `loki.fhirworx.io` | Log aggregation |
|
||||
|
||||
## Analytics platform
|
||||
|
||||
@@ -502,7 +502,7 @@ stack/
|
||||
|
||||
- Docker (rootless mode)
|
||||
- NVIDIA GPU with container toolkit
|
||||
- Domain or `/etc/hosts` entries for `*.homelab.fhirworx.io`
|
||||
- Domain or `/etc/hosts` entries for `*.fhirworx.io`
|
||||
|
||||
```bash
|
||||
# Fix for rootless NVIDIA containers
|
||||
@@ -514,7 +514,7 @@ sudo sed -i 's/#no-cgroups = false/no-cgroups = true/' /etc/nvidia-container-run
|
||||
```
|
||||
# ~/.ssh/config
|
||||
Host gitea
|
||||
HostName homelab.fhirworx.io
|
||||
HostName fhirworx.io
|
||||
Port 2222
|
||||
User git
|
||||
IdentityFile ~/.ssh/gitea_ed25519
|
||||
|
||||
109
compose.yml
109
compose.yml
@@ -5,23 +5,35 @@ networks:
|
||||
internal: false
|
||||
ipam:
|
||||
config:
|
||||
- subnet: 172.25.0.0/16
|
||||
- subnet: 192.168.5.0/24
|
||||
storage:
|
||||
name: storage
|
||||
driver: bridge
|
||||
internal: true
|
||||
ipam:
|
||||
config:
|
||||
- subnet: 192.168.6.0/24
|
||||
data:
|
||||
name: data
|
||||
driver: bridge
|
||||
internal: true
|
||||
ipam:
|
||||
config:
|
||||
- subnet: 192.168.7.0/24
|
||||
observability:
|
||||
name: observability
|
||||
driver: bridge
|
||||
internal: true
|
||||
ipam:
|
||||
config:
|
||||
- subnet: 192.168.8.0/24
|
||||
ci:
|
||||
name: ci
|
||||
driver: bridge
|
||||
internal: false
|
||||
ipam:
|
||||
config:
|
||||
- subnet: 192.168.9.0/24
|
||||
|
||||
services:
|
||||
coredns:
|
||||
@@ -30,7 +42,7 @@ services:
|
||||
command: ["-conf", "/etc/coredns/Corefile"]
|
||||
networks:
|
||||
gateway:
|
||||
ipv4_address: 172.25.0.53
|
||||
ipv4_address: 192.168.5.53
|
||||
volumes:
|
||||
- ./infra/coredns/Corefile:/etc/coredns/Corefile:ro
|
||||
- ./infra/coredns/hosts:/etc/coredns/hosts:ro
|
||||
@@ -54,7 +66,7 @@ services:
|
||||
- ./infra/traefik/certs:/etc/traefik/certs:ro
|
||||
- ./infra/traefik/plugins:/plugins-local:ro
|
||||
environment:
|
||||
- DOMAIN=${DOMAIN:-homelab.fhirworx.io}
|
||||
- DOMAIN=${DOMAIN:-fhirworx.io}
|
||||
- OTEL_SERVICE_NAME=traefik
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
@@ -149,10 +161,10 @@ services:
|
||||
- GITEA__packages__MINIO_SECRET_ACCESS_KEY=${GITEA_S3_SECRET_KEY}
|
||||
- GITEA__packages__MINIO_BUCKET=gitea-packages
|
||||
- GITEA__packages__MINIO_USE_SSL=false
|
||||
- GITEA__server__DOMAIN=gitea.${DOMAIN:-homelab.fhirworx.io}
|
||||
- GITEA__server__ROOT_URL=http://gitea.${DOMAIN:-homelab.fhirworx.io}/
|
||||
- GITEA__server__SSH_DOMAIN=${DOMAIN:-homelab.fhirworx.io}
|
||||
- GITEA__webhook__ALLOWED_HOST_LIST=woodpecker-server,ci.${DOMAIN:-homelab.fhirworx.io},${HOST_IP:-192.168.1.192},172.19.0.0/16
|
||||
- GITEA__server__DOMAIN=gitea.${DOMAIN:-fhirworx.io}
|
||||
- GITEA__server__ROOT_URL=https://gitea.${DOMAIN:-fhirworx.io}/
|
||||
- GITEA__server__SSH_DOMAIN=${DOMAIN:-fhirworx.io}
|
||||
- GITEA__webhook__ALLOWED_HOST_LIST=woodpecker-server,ci.${DOMAIN:-fhirworx.io},${HOST_IP:-192.168.1.192},172.19.0.0/16
|
||||
- GITEA__ui__THEMES=gitea-auto,gitea-light,gitea-dark,loch
|
||||
- GITEA__ui__DEFAULT_THEME=loch
|
||||
volumes:
|
||||
@@ -176,15 +188,15 @@ services:
|
||||
- storage
|
||||
- ci
|
||||
dns:
|
||||
- 172.25.0.53
|
||||
- 192.168.5.53
|
||||
environment:
|
||||
- WOODPECKER_HOST=http://ci.${DOMAIN:-homelab.fhirworx.io}
|
||||
- WOODPECKER_HOST=https://ci.${DOMAIN:-fhirworx.io}
|
||||
- WOODPECKER_GRPC_ADDR=:9000
|
||||
- WOODPECKER_LOG_LEVEL=debug
|
||||
- WOODPECKER_ADMIN=${WOODPECKER_ADMIN:-kert}
|
||||
- WOODPECKER_OPEN=true
|
||||
- WOODPECKER_GITEA=true
|
||||
- WOODPECKER_GITEA_URL=http://gitea.${DOMAIN:-homelab.fhirworx.io}
|
||||
- WOODPECKER_GITEA_URL=https://gitea.${DOMAIN:-fhirworx.io}
|
||||
- WOODPECKER_GITEA_CLIENT=${WOODPECKER_GITEA_CLIENT}
|
||||
- WOODPECKER_GITEA_SECRET=${WOODPECKER_GITEA_SECRET}
|
||||
- WOODPECKER_AGENT_SECRET=${WOODPECKER_AGENT_SECRET}
|
||||
@@ -622,7 +634,7 @@ services:
|
||||
- GF_USERS_ALLOW_SIGN_UP=false
|
||||
- GF_AUTH_ANONYMOUS_ENABLED=true
|
||||
- GF_AUTH_ANONYMOUS_ORG_ROLE=Viewer
|
||||
- GF_SERVER_ROOT_URL=http://grafana.${DOMAIN:-homelab.fhirworx.io}
|
||||
- GF_SERVER_ROOT_URL=https://grafana.${DOMAIN:-fhirworx.io}
|
||||
volumes:
|
||||
- ./infra/grafana/provisioning:/etc/grafana/provisioning:ro
|
||||
- ./infra/grafana/dashboards:/var/lib/grafana/dashboards:ro
|
||||
@@ -648,6 +660,81 @@ services:
|
||||
- no-new-privileges:true
|
||||
restart: unless-stopped
|
||||
|
||||
# wire — ephemeral bootstrap container. Creates Gitea admin, OAuth2 app,
|
||||
# oauth2-proxy credentials, tunnel config, DNS records. Idempotent.
|
||||
# docker compose run --rm wire
|
||||
wire:
|
||||
image: ghcr.io/astral-sh/uv:python3.13-bookworm-slim
|
||||
container_name: wire
|
||||
networks:
|
||||
- gateway
|
||||
- storage
|
||||
working_dir: /app
|
||||
env_file: .env
|
||||
volumes:
|
||||
- .:/app
|
||||
- ${DOCKER_SOCK:-/run/user/1000/docker.sock}:/var/run/docker.sock:ro
|
||||
environment:
|
||||
- UV_PROJECT_ENVIRONMENT=/tmp/.venv
|
||||
entrypoint: ["uv", "run", "python", "dev/scripts/bootstrap_sso.py"]
|
||||
profiles: ["tools"]
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
|
||||
auth-handler:
|
||||
image: nginx:alpine
|
||||
container_name: auth-handler
|
||||
networks:
|
||||
- gateway
|
||||
volumes:
|
||||
- ./infra/oauth2-proxy/auth-handler.conf:/etc/nginx/conf.d/default.conf:ro
|
||||
depends_on:
|
||||
- oauth2-proxy
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
restart: unless-stopped
|
||||
|
||||
oauth2-proxy:
|
||||
image: quay.io/oauth2-proxy/oauth2-proxy:latest
|
||||
container_name: oauth2-proxy
|
||||
networks:
|
||||
- gateway
|
||||
env_file:
|
||||
- path: .state/gitea/oauth2-proxy.env
|
||||
required: false
|
||||
environment:
|
||||
- OAUTH2_PROXY_HTTP_ADDRESS=0.0.0.0:4180
|
||||
- OAUTH2_PROXY_PROVIDER=oidc
|
||||
- OAUTH2_PROXY_PROVIDER_DISPLAY_NAME=fhirworx
|
||||
- OAUTH2_PROXY_OIDC_ISSUER_URL=https://gitea.${DOMAIN:-fhirworx.io}/
|
||||
- OAUTH2_PROXY_SKIP_OIDC_DISCOVERY=true
|
||||
- OAUTH2_PROXY_LOGIN_URL=https://gitea.${DOMAIN:-fhirworx.io}/login/oauth/authorize
|
||||
- OAUTH2_PROXY_REDEEM_URL=http://gitea:3000/login/oauth/access_token
|
||||
- OAUTH2_PROXY_OIDC_JWKS_URL=http://gitea:3000/login/oauth/keys
|
||||
- OAUTH2_PROXY_INSECURE_OIDC_SKIP_ISSUER_VERIFICATION=true
|
||||
- OAUTH2_PROXY_REDIRECT_URL=https://auth.${DOMAIN:-fhirworx.io}/oauth2/callback
|
||||
- OAUTH2_PROXY_COOKIE_DOMAINS=.${DOMAIN:-fhirworx.io}
|
||||
- OAUTH2_PROXY_WHITELIST_DOMAINS=.${DOMAIN:-fhirworx.io}
|
||||
- OAUTH2_PROXY_COOKIE_SECURE=true
|
||||
- OAUTH2_PROXY_COOKIE_SAMESITE=lax
|
||||
- OAUTH2_PROXY_COOKIE_NAME=_fhirworx_auth
|
||||
- OAUTH2_PROXY_SET_XAUTHREQUEST=true
|
||||
- OAUTH2_PROXY_REVERSE_PROXY=true
|
||||
- OAUTH2_PROXY_EMAIL_DOMAINS=*
|
||||
- OAUTH2_PROXY_SKIP_PROVIDER_BUTTON=true
|
||||
- OAUTH2_PROXY_CUSTOM_SIGN_IN_LOGO=-
|
||||
depends_on:
|
||||
- gitea
|
||||
healthcheck:
|
||||
test: ["CMD", "oauth2-proxy", "--version"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
start_period: 15s
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
restart: unless-stopped
|
||||
|
||||
cloudflared:
|
||||
image: cloudflare/cloudflared:latest
|
||||
container_name: cloudflared
|
||||
|
||||
394
dev/scripts/bootstrap_sso.py
Normal file
394
dev/scripts/bootstrap_sso.py
Normal file
@@ -0,0 +1,394 @@
|
||||
"""Bootstrap Gitea SSO — admin user, OAuth2 app, oauth2-proxy wiring.
|
||||
|
||||
Idempotent on cold start: creates the admin user and OAuth2 application
|
||||
if they don't exist, persists credentials to .state/gitea/, then restarts
|
||||
oauth2-proxy only when credentials change.
|
||||
|
||||
Usage:
|
||||
uv run python dev/scripts/bootstrap_sso.py
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import secrets
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import httpx
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
STATE = ROOT / ".state" / "gitea"
|
||||
OAUTH_STATE = STATE / "oauth.json"
|
||||
OAUTH2_PROXY_ENV = STATE / "oauth2-proxy.env"
|
||||
|
||||
DOMAIN = os.environ.get("DOMAIN", "fhirworx.io")
|
||||
GITEA_API = os.environ.get("GITEA_API", "http://gitea:3000/api/v1")
|
||||
ADMIN_USER = os.environ.get("WOODPECKER_ADMIN", "kert")
|
||||
ADMIN_PASS = os.environ.get("GITEA_ADMIN_PASSWORD", "")
|
||||
|
||||
|
||||
def _headers(token: str) -> dict:
|
||||
return {"Authorization": f"token {token}", "Content-Type": "application/json"}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# State persistence
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _load_state() -> dict:
|
||||
if OAUTH_STATE.exists():
|
||||
return json.loads(OAUTH_STATE.read_text())
|
||||
return {}
|
||||
|
||||
|
||||
def _save_state(state: dict) -> None:
|
||||
STATE.mkdir(parents=True, exist_ok=True)
|
||||
tmp = OAUTH_STATE.with_suffix(".tmp")
|
||||
tmp.write_text(json.dumps(state, indent=2) + "\n")
|
||||
tmp.rename(OAUTH_STATE)
|
||||
|
||||
|
||||
def _write_env_file(path: Path, content: str) -> bool:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
if path.exists() and path.read_text() == content:
|
||||
return False
|
||||
tmp = path.with_suffix(".tmp")
|
||||
tmp.write_text(content)
|
||||
tmp.rename(path)
|
||||
return True
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Admin user
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _ensure_admin(client: httpx.Client) -> str | None:
|
||||
r = client.get(f"{GITEA_API}/user", auth=(ADMIN_USER, ADMIN_PASS))
|
||||
if r.status_code == 200:
|
||||
print(f" ok: admin '{ADMIN_USER}' exists")
|
||||
elif r.status_code == 401:
|
||||
# Try to create; if user exists, change the password instead
|
||||
result = subprocess.run(
|
||||
["docker", "exec", "gitea", "gitea", "admin", "user", "create",
|
||||
"--username", ADMIN_USER,
|
||||
"--password", ADMIN_PASS,
|
||||
"--email", f"{ADMIN_USER}@{DOMAIN}",
|
||||
"--admin"],
|
||||
capture_output=True, text=True,
|
||||
)
|
||||
if result.returncode == 0:
|
||||
print(f" ok: admin '{ADMIN_USER}' created")
|
||||
elif "already exists" in result.stderr:
|
||||
subprocess.run(
|
||||
["docker", "exec", "gitea", "gitea", "admin", "user",
|
||||
"change-password", "--username", ADMIN_USER,
|
||||
"--password", ADMIN_PASS,
|
||||
"--must-change-password=false"],
|
||||
capture_output=True, text=True,
|
||||
)
|
||||
print(f" ok: admin '{ADMIN_USER}' password synced")
|
||||
else:
|
||||
print(f" FAIL: admin creation — {result.stderr.strip()}")
|
||||
return None
|
||||
else:
|
||||
print(f" FAIL: unexpected status {r.status_code}")
|
||||
return None
|
||||
return _ensure_token(client)
|
||||
|
||||
|
||||
def _ensure_token(client: httpx.Client) -> str | None:
|
||||
state = _load_state()
|
||||
if state.get("api_token"):
|
||||
r = client.get(f"{GITEA_API}/user", headers=_headers(state["api_token"]))
|
||||
if r.status_code == 200:
|
||||
return state["api_token"]
|
||||
|
||||
# Delete stale token if it exists
|
||||
r = client.get(
|
||||
f"{GITEA_API}/users/{ADMIN_USER}/tokens",
|
||||
auth=(ADMIN_USER, ADMIN_PASS),
|
||||
)
|
||||
if r.status_code == 200:
|
||||
for t in r.json():
|
||||
if t["name"] == "stack-wire":
|
||||
client.delete(
|
||||
f"{GITEA_API}/users/{ADMIN_USER}/tokens/{t['id']}",
|
||||
auth=(ADMIN_USER, ADMIN_PASS),
|
||||
)
|
||||
break
|
||||
|
||||
r = client.post(
|
||||
f"{GITEA_API}/users/{ADMIN_USER}/tokens",
|
||||
auth=(ADMIN_USER, ADMIN_PASS),
|
||||
json={"name": "stack-wire", "scopes": ["all"]},
|
||||
)
|
||||
if r.status_code == 201:
|
||||
token = r.json()["sha1"]
|
||||
state["api_token"] = token
|
||||
_save_state(state)
|
||||
return token
|
||||
|
||||
print(f" FAIL: token creation ({r.status_code})")
|
||||
return None
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# OAuth2 application
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _ensure_oauth_app(
|
||||
client: httpx.Client, token: str, name: str, redirect_uri: str,
|
||||
) -> tuple[str, str] | None:
|
||||
headers = _headers(token)
|
||||
state = _load_state()
|
||||
apps = state.setdefault("oauth_apps", {})
|
||||
|
||||
if name in apps:
|
||||
r = client.get(f"{GITEA_API}/user/applications/oauth2", headers=headers)
|
||||
if r.status_code == 200:
|
||||
for app in r.json():
|
||||
if app["client_id"] == apps[name]["client_id"]:
|
||||
print(f" ok: OAuth2 app '{name}' exists")
|
||||
return apps[name]["client_id"], apps[name]["client_secret"]
|
||||
del apps[name]
|
||||
|
||||
# Clean up stale app by name
|
||||
r = client.get(f"{GITEA_API}/user/applications/oauth2", headers=headers)
|
||||
if r.status_code == 200:
|
||||
for app in r.json():
|
||||
if app["name"] == name:
|
||||
client.delete(
|
||||
f"{GITEA_API}/user/applications/oauth2/{app['id']}",
|
||||
headers=headers,
|
||||
)
|
||||
break
|
||||
|
||||
r = client.post(
|
||||
f"{GITEA_API}/user/applications/oauth2",
|
||||
headers=headers,
|
||||
json={
|
||||
"name": name,
|
||||
"redirect_uris": [redirect_uri],
|
||||
"confidential_client": True,
|
||||
},
|
||||
)
|
||||
if r.status_code != 201:
|
||||
print(f" FAIL: OAuth2 app '{name}' ({r.status_code})")
|
||||
return None
|
||||
|
||||
data = r.json()
|
||||
cid, csec = data["client_id"], data["client_secret"]
|
||||
apps[name] = {"client_id": cid, "client_secret": csec}
|
||||
_save_state(state)
|
||||
print(f" ok: OAuth2 app '{name}' created")
|
||||
return cid, csec
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Downstream wiring
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _write_oauth2_proxy_env(client_id: str, client_secret: str) -> bool:
|
||||
state = _load_state()
|
||||
cookie_secret = state.get("oauth2_proxy_cookie_secret")
|
||||
if not cookie_secret or len(cookie_secret) != 32:
|
||||
cookie_secret = secrets.token_hex(16)
|
||||
state["oauth2_proxy_cookie_secret"] = cookie_secret
|
||||
_save_state(state)
|
||||
return _write_env_file(OAUTH2_PROXY_ENV, (
|
||||
f"OAUTH2_PROXY_CLIENT_ID={client_id}\n"
|
||||
f"OAUTH2_PROXY_CLIENT_SECRET={client_secret}\n"
|
||||
f"OAUTH2_PROXY_COOKIE_SECRET={cookie_secret}\n"
|
||||
))
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tunnel DNS (Cloudflare API via wrangler OAuth token)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
SUBDOMAINS = [
|
||||
"", "dashboard", "docs", "gitea", "ci", "notebooks", "zotero",
|
||||
"webdav", "api", "nessie", "trino", "polaris", "grafana",
|
||||
"prometheus", "jaeger", "loki", "s3", "s3console", "traefik", "auth",
|
||||
]
|
||||
|
||||
|
||||
def _clear_access_apps(client: httpx.Client) -> None:
|
||||
"""Remove Cloudflare Access apps — our oauth2-proxy handles auth instead."""
|
||||
cf_token = os.environ.get("CF_API_TOKEN")
|
||||
if not cf_token:
|
||||
print(" skip: CF_API_TOKEN not set")
|
||||
return
|
||||
account = "89f36257ec24ba34152e7c82a66335a3"
|
||||
headers = {"Authorization": f"Bearer {cf_token}"}
|
||||
r = client.get(
|
||||
f"https://api.cloudflare.com/client/v4/accounts/{account}/access/apps",
|
||||
headers=headers,
|
||||
)
|
||||
if not r.json().get("success"):
|
||||
print(" skip: cannot list Access apps")
|
||||
return
|
||||
deleted = 0
|
||||
for app in r.json()["result"]:
|
||||
domain = app.get("domain", "")
|
||||
# Delete apps on our domain (but keep Warp/Launcher)
|
||||
if domain.endswith(f".{DOMAIN}") or domain == DOMAIN:
|
||||
client.delete(
|
||||
f"https://api.cloudflare.com/client/v4/accounts/{account}/access/apps/{app['id']}",
|
||||
headers=headers,
|
||||
)
|
||||
deleted += 1
|
||||
if deleted:
|
||||
print(f" ok: removed {deleted} Access app(s)")
|
||||
else:
|
||||
print(" ok: no conflicting Access apps")
|
||||
|
||||
|
||||
def _sync_tunnel_dns(client: httpx.Client) -> None:
|
||||
"""Update the Cloudflare tunnel remote config with all service hostnames."""
|
||||
token = os.environ.get("CF_API_TOKEN")
|
||||
if not token:
|
||||
print(" skip: CF_API_TOKEN not set")
|
||||
return
|
||||
|
||||
account = "89f36257ec24ba34152e7c82a66335a3"
|
||||
tunnel = "1389035e-d3ba-4a4f-969d-a369c07ee057"
|
||||
api = f"https://api.cloudflare.com/client/v4/accounts/{account}/cfd_tunnel/{tunnel}/configurations"
|
||||
|
||||
ingress = []
|
||||
for sub in SUBDOMAINS:
|
||||
hostname = f"{sub}.{DOMAIN}" if sub else DOMAIN
|
||||
ingress.append({
|
||||
"hostname": hostname,
|
||||
"service": "http://traefik:80",
|
||||
"originRequest": {},
|
||||
})
|
||||
ingress.append({"service": "http_status:404", "originRequest": {}})
|
||||
|
||||
r = client.put(
|
||||
api,
|
||||
headers={"Authorization": f"Bearer {token}", "Content-Type": "application/json"},
|
||||
json={"config": {"ingress": ingress}},
|
||||
)
|
||||
if r.status_code == 200 and r.json().get("success"):
|
||||
print(f" ok: tunnel config updated ({len(SUBDOMAINS)} hostnames)")
|
||||
else:
|
||||
print(f" FAIL: tunnel config update ({r.status_code})")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Entry point
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def main() -> None:
|
||||
if not ADMIN_PASS:
|
||||
print("ERROR: GITEA_ADMIN_PASSWORD not set")
|
||||
sys.exit(1)
|
||||
|
||||
print("==> Waiting for Gitea...")
|
||||
with httpx.Client(timeout=10) as client:
|
||||
import time
|
||||
for _ in range(60):
|
||||
try:
|
||||
r = client.get(f"{GITEA_API}/settings/api")
|
||||
if r.status_code == 200:
|
||||
break
|
||||
except httpx.ConnectError:
|
||||
pass
|
||||
time.sleep(2)
|
||||
else:
|
||||
print("ERROR: Gitea did not start in 120s")
|
||||
sys.exit(1)
|
||||
|
||||
print("==> Configuring Gitea SSO")
|
||||
token = _ensure_admin(client)
|
||||
if not token:
|
||||
sys.exit(1)
|
||||
|
||||
creds = _ensure_oauth_app(
|
||||
client, token, "platform-sso",
|
||||
f"https://auth.{DOMAIN}/oauth2/callback",
|
||||
)
|
||||
if creds:
|
||||
changed = _write_oauth2_proxy_env(*creds)
|
||||
if changed:
|
||||
print(" ok: oauth2-proxy env updated — restarting")
|
||||
subprocess.run(
|
||||
["docker", "restart", "oauth2-proxy"],
|
||||
capture_output=True,
|
||||
)
|
||||
else:
|
||||
print(" ok: oauth2-proxy env unchanged")
|
||||
|
||||
print("==> Clearing Cloudflare Access apps")
|
||||
_clear_access_apps(client)
|
||||
|
||||
print("==> Syncing tunnel config")
|
||||
_sync_tunnel_dns(client)
|
||||
|
||||
print("==> Ensuring DNS records")
|
||||
_ensure_dns(client)
|
||||
|
||||
print("==> Done")
|
||||
|
||||
|
||||
def _ensure_dns(client: httpx.Client) -> None:
|
||||
"""Create missing Cloudflare DNS CNAME records pointing to the tunnel."""
|
||||
cf_token = os.environ.get("CF_API_TOKEN")
|
||||
if not cf_token:
|
||||
print(" skip: CF_API_TOKEN not set")
|
||||
return
|
||||
|
||||
zone = "f8553bde1ddb415b8c3e5dbec4b28330"
|
||||
tunnel = "1389035e-d3ba-4a4f-969d-a369c07ee057"
|
||||
tunnel_cname = f"{tunnel}.cfargotunnel.com"
|
||||
headers = {"Authorization": f"Bearer {cf_token}", "Content-Type": "application/json"}
|
||||
|
||||
# Fetch existing records
|
||||
r = client.get(
|
||||
f"https://api.cloudflare.com/client/v4/zones/{zone}/dns_records",
|
||||
headers=headers,
|
||||
params={"per_page": 100},
|
||||
)
|
||||
if not r.json().get("success"):
|
||||
err = r.json().get("errors", [{}])[0].get("message", "unknown")
|
||||
print(f" FAIL: DNS list — {err}")
|
||||
return
|
||||
|
||||
existing = {rec["name"] for rec in r.json()["result"]}
|
||||
|
||||
created = 0
|
||||
for sub in SUBDOMAINS:
|
||||
hostname = f"{sub}.{DOMAIN}" if sub else DOMAIN
|
||||
if hostname in existing:
|
||||
continue
|
||||
# Root domain needs different CNAME name
|
||||
name = hostname
|
||||
cr = client.post(
|
||||
f"https://api.cloudflare.com/client/v4/zones/{zone}/dns_records",
|
||||
headers=headers,
|
||||
json={
|
||||
"type": "CNAME",
|
||||
"name": name,
|
||||
"content": tunnel_cname,
|
||||
"proxied": True,
|
||||
"ttl": 1,
|
||||
},
|
||||
)
|
||||
if cr.json().get("success"):
|
||||
created += 1
|
||||
else:
|
||||
err = cr.json().get("errors", [{}])[0].get("message", "unknown")
|
||||
print(f" FAIL: {hostname} — {err}")
|
||||
|
||||
if created:
|
||||
print(f" ok: {created} DNS records created")
|
||||
else:
|
||||
print(f" ok: all {len(SUBDOMAINS)} DNS records exist")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -33,7 +33,7 @@ from conf import path as conf_path
|
||||
ZOTERO_DB = str(conf_path("db.zotero"))
|
||||
STORAGE_DIR = Path(conf_path("storage.zotero"))
|
||||
|
||||
UNPAYWALL_EMAIL = "dev@homelab.fhirworx.io"
|
||||
UNPAYWALL_EMAIL = "dev@fhirworx.io"
|
||||
USER_AGENT = "stack-pdf-fetcher/1.0 (mailto:{})".format(UNPAYWALL_EMAIL)
|
||||
|
||||
# Zotero key charset for generating attachment keys
|
||||
|
||||
@@ -49,4 +49,4 @@ echo " export NODE_EXTRA_CA_CERTS=$CERT"
|
||||
|
||||
echo ""
|
||||
echo "Done. Restart Firefox/Chrome to pick up the new CA."
|
||||
echo "HTTPS: https://docs.${DOMAIN:-homelab.fhirworx.io} should now load without warnings."
|
||||
echo "HTTPS: https://docs.${DOMAIN:-fhirworx.io} should now load without warnings."
|
||||
|
||||
@@ -5,7 +5,7 @@ const config = {
|
||||
title: "Stack",
|
||||
tagline: "Healthcare data platform documentation",
|
||||
favicon: "img/favicon.png",
|
||||
url: process.env.DOCS_URL || "http://docs.homelab.fhirworx.io",
|
||||
url: process.env.DOCS_URL || "https://docs.fhirworx.io",
|
||||
baseUrl: "/",
|
||||
onBrokenLinks: "warn",
|
||||
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
tunnel: 1389035e-d3ba-4a4f-969d-a369c07ee057
|
||||
credentials-file: /home/nonroot/.cloudflared/1389035e-d3ba-4a4f-969d-a369c07ee057.json
|
||||
|
||||
warp-routing:
|
||||
enabled: true
|
||||
|
||||
ingress:
|
||||
- service: socks-proxy
|
||||
originRequest:
|
||||
connectTimeout: 30s
|
||||
- hostname: "*.fhirworx.io"
|
||||
service: http://traefik:80
|
||||
- hostname: "fhirworx.io"
|
||||
service: http://traefik:80
|
||||
- service: http_status:404
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
homelab.fhirworx.io {
|
||||
fhirworx.io {
|
||||
hosts /etc/coredns/hosts {
|
||||
fallthrough
|
||||
}
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
192.168.1.192 homelab.fhirworx.io
|
||||
192.168.1.192 dashboard.homelab.fhirworx.io
|
||||
192.168.1.192 docs.homelab.fhirworx.io
|
||||
192.168.1.192 gitea.homelab.fhirworx.io
|
||||
192.168.1.192 ci.homelab.fhirworx.io
|
||||
192.168.1.192 notebooks.homelab.fhirworx.io
|
||||
192.168.1.192 zotero.homelab.fhirworx.io
|
||||
192.168.1.192 webdav.homelab.fhirworx.io
|
||||
192.168.1.192 api.homelab.fhirworx.io
|
||||
192.168.1.192 nessie.homelab.fhirworx.io
|
||||
192.168.1.192 trino.homelab.fhirworx.io
|
||||
192.168.1.192 polaris.homelab.fhirworx.io
|
||||
192.168.1.192 grafana.homelab.fhirworx.io
|
||||
192.168.1.192 prometheus.homelab.fhirworx.io
|
||||
192.168.1.192 jaeger.homelab.fhirworx.io
|
||||
192.168.1.192 loki.homelab.fhirworx.io
|
||||
192.168.1.192 s3.homelab.fhirworx.io
|
||||
192.168.1.192 s3console.homelab.fhirworx.io
|
||||
192.168.1.192 fhirworx.io
|
||||
192.168.1.192 dashboard.fhirworx.io
|
||||
192.168.1.192 docs.fhirworx.io
|
||||
192.168.1.192 gitea.fhirworx.io
|
||||
192.168.1.192 ci.fhirworx.io
|
||||
192.168.1.192 notebooks.fhirworx.io
|
||||
192.168.1.192 zotero.fhirworx.io
|
||||
192.168.1.192 webdav.fhirworx.io
|
||||
192.168.1.192 api.fhirworx.io
|
||||
192.168.1.192 nessie.fhirworx.io
|
||||
192.168.1.192 trino.fhirworx.io
|
||||
192.168.1.192 polaris.fhirworx.io
|
||||
192.168.1.192 grafana.fhirworx.io
|
||||
192.168.1.192 prometheus.fhirworx.io
|
||||
192.168.1.192 jaeger.fhirworx.io
|
||||
192.168.1.192 loki.fhirworx.io
|
||||
192.168.1.192 s3.fhirworx.io
|
||||
192.168.1.192 s3console.fhirworx.io
|
||||
192.168.1.192 traefik.fhirworx.io
|
||||
192.168.1.192 auth.fhirworx.io
|
||||
|
||||
@@ -3,24 +3,23 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>HOMELAB COMMAND CENTER</title>
|
||||
<title>FHIRWORX COMMAND CENTER</title>
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/fav32.png">
|
||||
<link rel="stylesheet" href="/dashboard.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<header>
|
||||
<h1>HOMELAB</h1>
|
||||
<h1>FHIRWORX</h1>
|
||||
<p class="subtitle">COMMAND CENTER</p>
|
||||
<div class="score-bar">
|
||||
<span class="score-item">SERVICES: 14</span>
|
||||
<span class="score-item">SERVICES: 15</span>
|
||||
<span class="score-item">STATUS: ONLINE</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="grid">
|
||||
<a data-subdomain="traefik" target="_blank" class="tile proxy">
|
||||
<span class="badge new">NEW</span>
|
||||
<span class="status"></span>
|
||||
<span class="icon">🔗</span>
|
||||
<h2 class="tile-title">TRAEFIK</h2>
|
||||
@@ -77,7 +76,6 @@
|
||||
</a>
|
||||
|
||||
<a data-subdomain="docs" target="_blank" class="tile research">
|
||||
<span class="badge new">NEW</span>
|
||||
<span class="status"></span>
|
||||
<span class="icon">📖</span>
|
||||
<h2 class="tile-title">DOCS</h2>
|
||||
@@ -107,7 +105,6 @@
|
||||
</a>
|
||||
|
||||
<a data-subdomain="polaris" target="_blank" class="tile catalog">
|
||||
<span class="badge new">NEW</span>
|
||||
<span class="status"></span>
|
||||
<span class="icon">⭐</span>
|
||||
<h2 class="tile-title">POLARIS</h2>
|
||||
@@ -151,24 +148,31 @@
|
||||
<span class="tile-port"></span>
|
||||
<div class="stripe"></div>
|
||||
</a>
|
||||
|
||||
<a data-subdomain="api" data-path="/health" target="_blank" class="tile proxy">
|
||||
<span class="status"></span>
|
||||
<span class="icon">🔌</span>
|
||||
<h2 class="tile-title">API</h2>
|
||||
<p class="tile-desc">FastAPI platform backend</p>
|
||||
<span class="tile-port"></span>
|
||||
<div class="stripe"></div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<footer>
|
||||
MADE WITH <span>♥</span> FOR THE HOMELAB
|
||||
MADE WITH <span>♥</span> FOR FHIRWORX
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Domain resolution: derive base domain from current hostname
|
||||
// dashboard.homelab.fhirworx.io -> homelab.fhirworx.io
|
||||
// homelab.fhirworx.io -> homelab.fhirworx.io
|
||||
// All tiles use https://{subdomain}.fhirworx.io
|
||||
(function() {
|
||||
var base = location.hostname.replace(/^dashboard\./, '');
|
||||
document.querySelectorAll('[data-subdomain]').forEach(function(el) {
|
||||
var sub = el.dataset.subdomain;
|
||||
var path = el.dataset.path || '';
|
||||
var fqdn = sub + '.' + base;
|
||||
el.href = location.protocol + '//' + fqdn + path;
|
||||
el.href = 'https://' + fqdn + path;
|
||||
var port = el.querySelector('.tile-port');
|
||||
if (port) port.textContent = fqdn;
|
||||
});
|
||||
|
||||
39
infra/oauth2-proxy/auth-handler.conf
Normal file
39
infra/oauth2-proxy/auth-handler.conf
Normal file
@@ -0,0 +1,39 @@
|
||||
# Auth handler for Traefik ForwardAuth → oauth2-proxy.
|
||||
#
|
||||
# Traefik ForwardAuth sends the request here. nginx checks the session
|
||||
# cookie via oauth2-proxy /oauth2/auth:
|
||||
# - authenticated → 200 (Traefik grants access)
|
||||
# - unauthenticated → 302 to oauth2-proxy sign_in (browser follows redirect)
|
||||
#
|
||||
# This is the Traefik equivalent of nginx auth_request + error_page 401.
|
||||
|
||||
resolver 127.0.0.11 valid=15s ipv6=off;
|
||||
|
||||
server {
|
||||
listen 4181;
|
||||
|
||||
set $oauth2 http://oauth2-proxy:4180;
|
||||
|
||||
location / {
|
||||
proxy_pass $oauth2/oauth2/auth;
|
||||
proxy_set_header Host auth.$host_domain;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-Proto https;
|
||||
proxy_set_header X-Forwarded-Host $http_x_forwarded_host;
|
||||
proxy_pass_request_body off;
|
||||
proxy_set_header Content-Length "";
|
||||
|
||||
proxy_intercept_errors on;
|
||||
error_page 401 = @signin;
|
||||
}
|
||||
|
||||
location @signin {
|
||||
return 302 https://auth.$host_domain/oauth2/start?rd=https://$http_x_forwarded_host$request_uri;
|
||||
}
|
||||
}
|
||||
|
||||
# Extract base domain from X-Forwarded-Host (e.g. docs.fhirworx.io → fhirworx.io)
|
||||
map $http_x_forwarded_host $host_domain {
|
||||
~^[^.]+\.(.+)$ $1;
|
||||
default $http_x_forwarded_host;
|
||||
}
|
||||
@@ -1,24 +1,24 @@
|
||||
{{- $domain := env "DOMAIN" | default "homelab.fhirworx.io" -}}
|
||||
{{- $domain := env "DOMAIN" | default "fhirworx.io" -}}
|
||||
{{- $reef := dict
|
||||
"dashboard" (dict "port" "80" "theme" true "extra_hosts" (list $domain) "mw" "secure-headers")
|
||||
"docs" (dict "port" "80" "theme" true "mw" "secure-headers")
|
||||
"dashboard" (dict "port" "80" "theme" true "extra_hosts" (list $domain) "mw" "gitea-sso,secure-headers")
|
||||
"docs" (dict "port" "80" "theme" true "mw" "gitea-sso,secure-headers")
|
||||
"gitea" (dict "port" "3000" "theme" true "mw" "secure-headers")
|
||||
"woodpecker-server" (dict "port" "8000" "theme" true "subdomain" "ci" "mw" "local-only,secure-headers")
|
||||
"notebooks" (dict "port" "2718" "theme" true "mw" "local-only,secure-headers")
|
||||
"zotero" (dict "port" "8080" "theme" true "mw" "secure-headers")
|
||||
"woodpecker-server" (dict "port" "8000" "theme" true "subdomain" "ci" "mw" "gitea-sso,secure-headers")
|
||||
"notebooks" (dict "port" "2718" "theme" true "mw" "gitea-sso,secure-headers")
|
||||
"zotero" (dict "port" "8080" "theme" true "mw" "gitea-sso,secure-headers")
|
||||
"webdav" (dict "port" "8080" "theme" false "mw" "secure-headers")
|
||||
"api" (dict "port" "8000" "theme" false "mw" "secure-headers")
|
||||
"nessie" (dict "port" "19120" "theme" false "mw" "local-only,infra-headers")
|
||||
"trino" (dict "port" "8080" "theme" true "mw" "local-only,infra-headers")
|
||||
"polaris" (dict "port" "8181" "theme" false "mw" "local-only,infra-headers")
|
||||
"grafana" (dict "port" "3000" "theme" true "mw" "local-only,secure-headers")
|
||||
"prometheus" (dict "port" "9090" "theme" true "mw" "local-only,infra-headers")
|
||||
"jaeger" (dict "port" "16686" "theme" true "mw" "local-only,infra-headers")
|
||||
"loki" (dict "port" "3100" "theme" false "mw" "local-only,infra-headers")
|
||||
"nessie" (dict "port" "19120" "theme" false "mw" "gitea-sso,infra-headers")
|
||||
"trino" (dict "port" "8080" "theme" true "mw" "gitea-sso,infra-headers")
|
||||
"polaris" (dict "port" "8181" "theme" false "mw" "gitea-sso,infra-headers")
|
||||
"grafana" (dict "port" "3000" "theme" true "mw" "gitea-sso,secure-headers")
|
||||
"prometheus" (dict "port" "9090" "theme" true "mw" "gitea-sso,infra-headers")
|
||||
"jaeger" (dict "port" "16686" "theme" true "mw" "gitea-sso,infra-headers")
|
||||
"loki" (dict "port" "3100" "theme" false "mw" "gitea-sso,infra-headers")
|
||||
-}}
|
||||
{{- $multi := dict
|
||||
"rustfs-api" (dict "container" "rustfs" "port" "9000" "subdomain" "s3" "theme" false "mw" "local-only,infra-headers")
|
||||
"rustfs-console" (dict "container" "rustfs" "port" "9001" "subdomain" "s3console" "theme" true "mw" "local-only,infra-headers")
|
||||
"rustfs-api" (dict "container" "rustfs" "port" "9000" "subdomain" "s3" "theme" false "mw" "gitea-sso,infra-headers")
|
||||
"rustfs-console" (dict "container" "rustfs" "port" "9001" "subdomain" "s3console" "theme" true "mw" "gitea-sso,infra-headers")
|
||||
-}}
|
||||
http:
|
||||
middlewares:
|
||||
@@ -27,13 +27,15 @@ http:
|
||||
frameDeny: true
|
||||
browserXssFilter: true
|
||||
contentTypeNosniff: true
|
||||
local-only:
|
||||
ipAllowList:
|
||||
sourceRange:
|
||||
- "127.0.0.1/32"
|
||||
- "10.0.0.0/8"
|
||||
- "172.16.0.0/12"
|
||||
- "192.168.0.0/16"
|
||||
# SSO: auth-handler (nginx) wraps oauth2-proxy to convert 401 → 302.
|
||||
# Same logic as corwins.media: auth_request + error_page 401 = @signin.
|
||||
gitea-sso:
|
||||
forwardAuth:
|
||||
address: "http://auth-handler:4181"
|
||||
trustForwardHeader: true
|
||||
authResponseHeaders:
|
||||
- "X-Auth-Request-User"
|
||||
- "X-Auth-Request-Email"
|
||||
infra-headers:
|
||||
headers:
|
||||
frameDeny: true
|
||||
@@ -48,8 +50,20 @@ http:
|
||||
lastModified: true
|
||||
rewrites:
|
||||
- regex: "</head>"
|
||||
replacement: '<link rel="icon" type="image/png" sizes="32x32" href="http://{{ $domain }}/fav32.png"><link rel="stylesheet" type="text/css" href="http://{{ $domain }}/loch.css"></head>'
|
||||
replacement: '<link rel="icon" type="image/png" sizes="32x32" href="//{{ $domain }}/fav32.png"><link rel="stylesheet" type="text/css" href="//{{ $domain }}/loch.css"></head>'
|
||||
routers:
|
||||
# auth.DOMAIN — oauth2-proxy public endpoints (login, callback, sign_out)
|
||||
auth-host:
|
||||
rule: "Host(`auth.{{ $domain }}`)"
|
||||
service: oauth2-proxy
|
||||
entryPoints:
|
||||
- web
|
||||
auth-host-tls:
|
||||
rule: "Host(`auth.{{ $domain }}`)"
|
||||
service: oauth2-proxy
|
||||
entryPoints:
|
||||
- websecure
|
||||
tls: {}
|
||||
{{- range $name, $svc := $reef }}
|
||||
{{ $name }}:
|
||||
rule: "Host(`{{ get $svc "subdomain" | default $name }}.{{ $domain }}`){{ range get $svc "extra_hosts" | default list }} || Host(`{{ . }}`){{ end }}"
|
||||
@@ -115,7 +129,7 @@ http:
|
||||
- web
|
||||
middlewares:
|
||||
- inject-loch
|
||||
- local-only
|
||||
- gitea-sso
|
||||
- infra-headers
|
||||
traefik-dashboard-tls:
|
||||
rule: "Host(`traefik.{{ $domain }}`)"
|
||||
@@ -125,7 +139,7 @@ http:
|
||||
tls: {}
|
||||
middlewares:
|
||||
- inject-loch
|
||||
- local-only
|
||||
- gitea-sso
|
||||
- infra-headers
|
||||
services:
|
||||
{{- range $name, $svc := $reef }}
|
||||
@@ -140,3 +154,7 @@ http:
|
||||
servers:
|
||||
- url: "http://{{ get $svc "container" }}:{{ get $svc "port" }}"
|
||||
{{- end }}
|
||||
oauth2-proxy:
|
||||
loadBalancer:
|
||||
servers:
|
||||
- url: "http://oauth2-proxy:4180"
|
||||
|
||||
@@ -14,8 +14,8 @@
|
||||
# connection details for the selected backend.
|
||||
|
||||
[platform]
|
||||
domain = "homelab.fhirworx.io"
|
||||
registry = "gitea.homelab.fhirworx.io"
|
||||
domain = "fhirworx.io"
|
||||
registry = "gitea.fhirworx.io"
|
||||
image_prefix = "fhirworx"
|
||||
repo = "homelab/stack"
|
||||
ci_email = "ci@fhirworx.io"
|
||||
|
||||
@@ -17,7 +17,7 @@ from api.diag.vuln import (
|
||||
)
|
||||
|
||||
SAMPLE_TRIVY = {
|
||||
"ArtifactName": "gitea.homelab.fhirworx.io/homelab/notebooks:sha-abc12345",
|
||||
"ArtifactName": "gitea.fhirworx.io/homelab/notebooks:sha-abc12345",
|
||||
"Results": [
|
||||
{
|
||||
"Vulnerabilities": [
|
||||
|
||||
@@ -219,7 +219,8 @@ class TestVerify:
|
||||
|
||||
def test_verify_wildcard_san(self) -> None:
|
||||
info = verify_wildcard()
|
||||
assert "*.homelab.fhirworx.io" in info.get("san", "")
|
||||
san = info.get("san", "")
|
||||
assert "fhirworx.io" in san
|
||||
|
||||
def test_verify_wildcard_issuer_is_ca(self) -> None:
|
||||
info = verify_wildcard()
|
||||
|
||||
Reference in New Issue
Block a user