HTTPS через Caddy с автоматическими сертификатами

Токен и расшифровки разговоров ходили по сети открытым текстом. Теперь в
пакете лежит Caddy: он держит сертификаты Let's Encrypt и продлевает их сам,
а сервис уходит на localhost.

Отдельно решён вопрос подмены адреса: за прокси все запросы приходят с
localhost, и список разрешённых адресов пускал бы кого угодно. Заголовку
с настоящим адресом сервис верит только от доверенного прокси - на это
восемь тестов.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Vladimir Bryzgalov
2026-08-16 01:41:34 +05:00
co-authored by Claude Opus 5
parent 64ce562bb1
commit c992b7907d
6 changed files with 198 additions and 5 deletions
+11 -4
View File
@@ -19,7 +19,7 @@ from app.config import Settings, load_settings
from app.logbuffer import get as log_buffer
from app.logbuffer import install as install_log_buffer
from app.pipeline import ModelsMissing, Pipeline, to_wav16k
from app.security import check_token, ip_allowed, parse_allowlist
from app.security import check_token, client_address, ip_allowed, parse_allowlist
from app.store import JobStatus, JobStore
from app.webhook import deliver_async
from app.version import __version__
@@ -28,6 +28,7 @@ log = logging.getLogger("talkscore-asr")
settings: Settings = load_settings()
allowlist = parse_allowlist(settings.allow_ips)
trusted_proxies = parse_allowlist(settings.trust_proxy)
# Хранилище создаётся не здесь, а при старте сервиса. На Windows процессы
# поднимаются через spawn и заново импортируют этот модуль: если бы очередь
@@ -190,16 +191,22 @@ app = FastAPI(
bearer_scheme = HTTPBearer(description="Токен из config.toml, раздел [security]")
def peer_address(request: Request) -> str | None:
"""Адрес клиента с поправкой на обратный прокси."""
peer = request.client.host if request.client else None
return client_address(peer, request.headers.get("x-forwarded-for"), trusted_proxies)
def ip_guard(request: Request) -> None:
"""Только проверка адреса: нужна для страницы с описанием методов."""
client_ip = request.client.host if request.client else None
client_ip = peer_address(request)
if not ip_allowed(client_ip, allowlist):
raise HTTPException(status_code=403, detail="адрес не в списке разрешённых")
def guard(request: Request) -> None:
"""Проверяет адрес и токен. Порядок важен: сначала сеть, потом секрет."""
client_ip = request.client.host if request.client else None
client_ip = peer_address(request)
if not ip_allowed(client_ip, allowlist):
log.warning("отказано по адресу: %s", client_ip)
raise HTTPException(status_code=403, detail="адрес не в списке разрешённых")
@@ -253,7 +260,7 @@ def health(request: Request) -> JSONResponse:
отладка отказов превращается в гадание, а сам адрес клиенту и так известен.
Список разрешённых адресов при этом не раскрывается.
"""
client_ip = request.client.host if request.client else None
client_ip = peer_address(request)
return JSONResponse({
"status": "ok" if _state["ready"] else "no_models",
"version": __version__,