Compare commits

..
2 Commits
4 changed files with 314 additions and 3 deletions
+292
View File
@@ -0,0 +1,292 @@
# ╔═══════════════════════════════════════════════════════════════════╗
# ║ CADDY REVERSE PROXY — Thay thế Traefik baremetal ║
# ║ Server: Ubuntu 26.04 | Backend: 192.168.10.101 ║
# ║ CDN: Cloudflare (Proxy mode, SSL = Full Strict) ║
# ╚═══════════════════════════════════════════════════════════════════╝
# ═══════════════════════════════════════════════════════════════
# GLOBAL OPTIONS
# Thay thế: traefik.yml (global, entryPoints, certificatesResolvers, log)
# ═══════════════════════════════════════════════════════════════
{
# Email ACME/Let's Encrypt
email lhuydung26@lhdsin.net
# Cấu hình xin SSL (Let's Encrypt) tự động qua DNS của Cloudflare cho toàn bộ hệ thống
acme_dns cloudflare {env.CLOUDFLARE_API_TOKEN}
# Bật Prometheus metrics
metrics
# Admin API
admin localhost:2019
# TỰ ĐỘNG nhận Real Client IP từ Cloudflare Proxy (yêu cầu plugin caddy-cloudflare-ip)
servers {
trusted_proxies cloudflare
trusted_proxies_strict
}
}
# ═══════════════════════════════════════════════════════════════
# METRICS ENDPOINT
# Thay thế: entryPoints.metrics (port :8084) + metrics.prometheus
# Prometheus scrape target: http://<server-ip>:8084/metrics
# ═══════════════════════════════════════════════════════════════
:8084 {
metrics /metrics
}
# ═══════════════════════════════════════════════════════════════
# SNIPPET: Authentik Forward Auth
# Thay thế: middlewares.authentik-auth.forwardAuth (dashboard.yml)
# Dùng lại bằng: import authentik
#
# ⚠️ LƯU Ý: URI kết thúc bằng /auth/caddy (KHÔNG phải /auth/traefik)
# Cần cập nhật Proxy Provider trong Authentik admin panel
# ═══════════════════════════════════════════════════════════════
(authentik) {
forward_auth http://192.168.10.101:9000 {
uri /outpost.goauthentik.io/auth/caddy
# Headers Authentik trả về sau xác thực thành công
# Caddy sẽ forward tới backend service
copy_headers X-Authentik-Username X-Authentik-Groups X-Authentik-Entitlements X-Authentik-Email X-Authentik-Name X-Authentik-Uid X-Authentik-Jwt X-Authentik-Meta-Jwks X-Authentik-Meta-Outpost X-Authentik-Meta-Provider X-Authentik-Meta-App X-Authentik-Meta-Version
trusted_proxies private_ranges
}
}
# ═══════════════════════════════════════════════════════════════
# SNIPPET: Access Log chuẩn (tái sử dụng)
# Thay thế: accessLog (traefik.yml)
# ═══════════════════════════════════════════════════════════════
(access_log) {
log {
output file /var/log/caddy/access-{args[0]}.log {
roll_size 100MiB
roll_keep 5
roll_keep_for 168h
}
format json
}
}
# ╔═══════════════════════════════════════════════════════════════════╗
# ║ SITE BLOCKS — Thay thế toàn bộ thư mục dynamic/ ║
# ╚═══════════════════════════════════════════════════════════════════╝
# ───────────────────────────────────────────────────────────────
# 1. AUTHENTIK SSO
# Thay thế: dynamic/authentik.yml
# Backend: http://192.168.10.101:9000
# Bao gồm: Web UI + WebSocket (/ws/) + Outpost paths
# ───────────────────────────────────────────────────────────────
sso.lhdsin.net {
reverse_proxy http://192.168.10.101:9000
tracing {
span sso
}
import access_log sso
}
# ───────────────────────────────────────────────────────────────
# 2. GRAFANA (bảo vệ bởi Authentik Forward Auth)
# Thay thế: dynamic/grafana.yml + authentik-outpost-apps.yml (grafana-outpost-router)
# Backend: http://192.168.10.101:3002
#
# ⚠️ Phải dùng route {} để đảm bảo thứ tự thực thi:
# 1. Outpost callback (KHÔNG qua auth → tránh redirect loop)
# 2. Forward Auth kiểm tra xác thực
# 3. Reverse proxy tới Grafana
# ───────────────────────────────────────────────────────────────
dash.lhdsin.net {
route {
# Outpost callback — ⚠️ KHÔNG gắn forward_auth ở đây!
reverse_proxy /outpost.goauthentik.io/* http://192.168.10.101:9000
# Forward Auth → Authentik kiểm tra session
import authentik
# Backend Grafana
reverse_proxy http://192.168.10.101:3002
}
tracing {
span grafana
}
import access_log grafana
}
# ───────────────────────────────────────────────────────────────
# 3. HOME ASSISTANT
# Thay thế: dynamic/hass.yml
# Backend: http://192.168.10.101:8123
# Caddy tự động xử lý WebSocket upgrade (không cần cấu hình thêm)
# ───────────────────────────────────────────────────────────────
hass.lhdsin.net {
reverse_proxy http://192.168.10.101:8123
tracing {
span hass
}
import access_log hass
}
# ───────────────────────────────────────────────────────────────
# 4. NAS / SYNOLOGY DSM (Backend HTTPS với self-signed cert)
# Thay thế: dynamic/nas.yml
# Backend: https://192.168.10.101:5001
#
# tls_insecure_skip_verify: Bỏ qua xác thực SSL backend
# Thay thế: serversTransports.insecureBackendTransport.insecureSkipVerify
# ───────────────────────────────────────────────────────────────
nas.lhdsin.net {
reverse_proxy https://192.168.10.101:5001 {
transport http {
tls_insecure_skip_verify
}
# Ép DSM luôn phục vụ giao diện Desktop
header_up User-Agent "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36"
}
tracing {
span nas
}
import access_log nas
}
# ───────────────────────────────────────────────────────────────
# 5. OPENSPEEDTEST
# Thay thế: qBittorrent (đã tạm dừng)
# Backend: http://192.168.10.101:3000
# ───────────────────────────────────────────────────────────────
speed.lhdsin.net {
# Public hoàn toàn, không đi qua Authentik
# Tắt flush buffer và cấu hình lại request_buffers để sửa lỗi Upload ảo của OpenSpeedtest
reverse_proxy http://192.168.10.101:3010 {
flush_interval -1
request_buffers 35MiB
}
tracing {
span speedtest
}
import access_log speedtest
}
# ───────────────────────────────────────────────────────────────
# 6. SHLINK (URL Shortener)
# Thay thế: dynamic/shlink.yml
# Backend: http://192.168.10.101:8082
# ───────────────────────────────────────────────────────────────
link.lhdsin.net {
reverse_proxy http://192.168.10.101:8082
tracing {
span shlink
}
import access_log shlink
}
# ───────────────────────────────────────────────────────────────
# 7. VAULTWARDEN
# Thay thế: dynamic/vaults.yml
# Backend: http://192.168.10.101:11001
#
# Caddy tự động xử lý WebSocket upgrade cho /notifications/hub
# Không cần router riêng như Traefik (vaults-websocket-router)
# ───────────────────────────────────────────────────────────────
vaults.lhdsin.net {
reverse_proxy http://192.168.10.101:11001
tracing {
span vaultwarden
}
import access_log vaults
}
# ───────────────────────────────────────────────────────────────
# 8. SHLINK WEB CLIENT (Bảo vệ bởi Authentik Forward Auth)
# Domain: shl.lhdsin.net
# Backend: http://192.168.10.101:8083
# ───────────────────────────────────────────────────────────────
shl.lhdsin.net {
route {
# Outpost callback — KHÔNG gắn forward_auth ở đây để tránh lỗi redirect loop
reverse_proxy /outpost.goauthentik.io/* http://192.168.10.101:9000
# Forward Auth → Authentik kiểm tra session
import authentik
# Backend Shlink Web Client
reverse_proxy http://192.168.10.101:8083
}
tracing {
span shlink-web
}
import access_log shlink-web
}
# ───────────────────────────────────────────────────────────────
# 9. KIBANA (Bảo vệ bởi Authentik Forward Auth)
# Domain: elk.lhdsin.net
# Backend: http://192.168.10.101:5601
# ───────────────────────────────────────────────────────────────
elk.lhdsin.net {
route {
# Outpost callback — KHÔNG gắn forward_auth ở đây để tránh lỗi redirect loop
reverse_proxy /outpost.goauthentik.io/* http://192.168.10.101:9000
# Forward Auth → Authentik kiểm tra session
import authentik
# Backend Kibana
reverse_proxy http://192.168.10.101:5601
}
tracing {
span kibana-web
}
import access_log kibana-web
}
# ───────────────────────────────────────────────────────────────
# 10. Gitea
# Backend: http://192.168.10.101:3005
# ───────────────────────────────────────────────────────────────
git.lhdsin.net {
reverse_proxy http://192.168.10.101:3005
tracing {
span gitea
}
import access_log gitea
}
# ───────────────────────────────────────────────────────────────
# 11. AUDIT HARDENING TOOL
# Public URL: https://audit.lhdsin.net/audit/
# Backend: http://192.168.10.101:8888
# handle_path loại bỏ prefix /audit trước khi chuyển tới FastAPI.
# ───────────────────────────────────────────────────────────────
audit.lhdsin.net {
redir / /audit/ 308
redir /audit /audit/ 308
handle_path /audit/* {
reverse_proxy http://192.168.10.101:8888
}
respond 404
tracing {
span audit-hardening-tool
}
import access_log audit-hardening-tool
}
+4
View File
@@ -189,7 +189,11 @@
<span class="header-user-role">{{ user_info.role|upper }} {% if user_info.unit_name %} | {{ user_info.unit_name }}{% endif %}</span> <span class="header-user-role">{{ user_info.role|upper }} {% if user_info.unit_name %} | {{ user_info.unit_name }}{% endif %}</span>
{% endif %} {% endif %}
</div> </div>
{% if user_info.role == 'dev_guest' %}
<a href="{{ url_for('login') }}" class="header-action-btn">🔐 Đăng nhập Admin/Analyzer</a>
{% else %}
<a href="{{ url_for('logout') }}" class="header-action-btn">🚪 Đăng xuất</a> <a href="{{ url_for('logout') }}" class="header-action-btn">🚪 Đăng xuất</a>
{% endif %}
{% else %} {% else %}
<div class="header-guest"> <div class="header-guest">
👤 Khách 👤 Khách
+3 -3
View File
@@ -845,9 +845,9 @@ def get_current_user(credentials: HTTPBasicCredentials = Depends(security)) -> O
@app.get("/login", response_class=HTMLResponse, name="login") @app.get("/login", response_class=HTMLResponse, name="login")
async def login_page(request: Request, next: str = "/"): async def login_page(request: Request, next: str = "/"):
"""Trang đăng nhập — nếu đã login rồi thì redirect ngay""" """Trang đăng nhập; dev_guest vẫn được phép chuyển sang tài khoản thật."""
# Nếu đã đăng nhập thì không cần vào trang login nữa current_user = get_session_user(request)
if get_session_user(request): if current_user and current_user.get("role") != "dev_guest":
return RedirectResponse(url=next, status_code=303) return RedirectResponse(url=next, status_code=303)
return templates.TemplateResponse( return templates.TemplateResponse(
"login.html", "login.html",
+15
View File
@@ -0,0 +1,15 @@
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
APP = (ROOT / "app" / "web_app_fastapi.py").read_text(encoding="utf-8")
HEADER = (ROOT / "app" / "templates" / "header.html").read_text(encoding="utf-8")
def test_dev_guest_is_allowed_to_open_login_page():
assert 'current_user.get("role") != "dev_guest"' in APP
def test_dev_guest_header_offers_real_login_instead_of_logout():
assert "user_info.role == 'dev_guest'" in HEADER
assert "url_for('login')" in HEADER