Перенесены секреты из .env в SM

This commit is contained in:
mi
2026-07-30 19:22:48 +03:00
parent 049c45db5c
commit e24ed9d8ef
58 changed files with 3350 additions and 1054 deletions
+3 -2
View File
@@ -13,7 +13,8 @@ COPY --from=builder /build/dist/*.whl /tmp/
RUN pip install --no-cache-dir /tmp/*.whl && rm -f /tmp/*.whl
COPY alembic.ini ./
COPY alembic ./alembic
COPY --chmod=0555 container-entrypoint.sh /usr/local/bin/han-container-entrypoint
USER 10001:10001
EXPOSE 8000
ENTRYPOINT ["uvicorn"]
CMD ["app.main:app", "--host", "0.0.0.0", "--port", "8000", "--no-proxy-headers"]
ENTRYPOINT ["/usr/local/bin/han-container-entrypoint"]
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--no-proxy-headers"]
+2 -2
View File
@@ -1,4 +1,5 @@
import asyncio
import os
from logging.config import fileConfig
from sqlalchemy import pool
@@ -6,12 +7,11 @@ from sqlalchemy import pool
from alembic import context
from app.db import Base
from app.postgres import create_postgres_engine
from app.settings import get_settings
config = context.config
if config.config_file_name:
fileConfig(config.config_file_name)
database_url = get_settings().database_url
database_url = os.environ["DATABASE_URL"]
config.set_main_option("sqlalchemy.url", database_url.replace("%", "%%"))
target_metadata = Base.metadata
@@ -0,0 +1,47 @@
from __future__ import annotations
import re
from collections.abc import Mapping
from typing import Any
REDACTED = "[REDACTED]"
_SENSITIVE_KEY = re.compile(
r"(authorization|cookie|password|passwd|secret|token|api[_-]?key|"
r"database[_-]?url|redis[_-]?url|dsn|callback[_-]?url)",
re.IGNORECASE,
)
_URI_USERINFO = re.compile(r"(?P<scheme>[a-z][a-z0-9+.-]*://)[^/@\s]+@", re.IGNORECASE)
_QUERY_SECRET = re.compile(
r"(?P<prefix>[?&](?:token|access_token|api_key|key|secret|password)=)[^&#\s]+",
re.IGNORECASE,
)
_AUTH_VALUE = re.compile(r"\b(Bearer|Basic)\s+[A-Za-z0-9._~+/=-]+", re.IGNORECASE)
def sanitize_text(value: str) -> str:
value = _URI_USERINFO.sub(r"\g<scheme>[REDACTED]@", value)
value = _QUERY_SECRET.sub(r"\g<prefix>[REDACTED]", value)
return _AUTH_VALUE.sub(r"\1 [REDACTED]", value)
def sanitize_value(value: Any) -> Any:
if isinstance(value, str):
return sanitize_text(value)
if isinstance(value, Mapping):
return {
str(key): REDACTED if _SENSITIVE_KEY.search(str(key)) else sanitize_value(item)
for key, item in value.items()
}
if isinstance(value, list):
return [sanitize_value(item) for item in value]
if isinstance(value, tuple):
return tuple(sanitize_value(item) for item in value)
return value
def redact_event(
_logger: Any,
_method_name: str,
event_dict: dict[str, Any],
) -> dict[str, Any]:
return sanitize_value(event_dict)
+7 -1
View File
@@ -50,6 +50,7 @@ from app.integrations import (
S3Client,
SafetyClient,
)
from app.logging_security import redact_event
from app.metrics import AUTH_BOOTSTRAP, HTTP_DURATION, HTTP_REQUESTS, RATE_LIMIT_DECISIONS
from app.notification_routes import router as notification_router
from app.notification_service import synchronize_source_tokens
@@ -96,6 +97,7 @@ def configure_logging(level: str) -> None:
processors=[
structlog.contextvars.merge_contextvars,
add_trace_context,
redact_event,
structlog.processors.TimeStamper(fmt="iso", utc=True, key="timestamp"),
structlog.stdlib.add_log_level,
structlog.processors.JSONRenderer(),
@@ -342,7 +344,11 @@ async def http_error(request: Request, exc: StarletteHTTPException):
@app.exception_handler(Exception)
async def unhandled_error(request: Request, exc: Exception):
log.exception("request.failed", error_code="internal_error")
log.error(
"request.failed",
error_code="internal_error",
error_type=type(exc).__name__,
)
return error_response(request, "internal_error", "Internal server error", 500)
@@ -0,0 +1,23 @@
#!/bin/sh
set -eu
for name in ${HAN_SECRET_VARS:-}; do
case "$name" in
""|[0-9]*|*[!A-Z0-9_]*)
echo "container secrets: invalid variable name" >&2
exit 64
;;
*) ;;
esac
eval "file=\${${name}_FILE:-}"
if [ -z "$file" ] || [ ! -r "$file" ]; then
echo "container secrets: missing file for $name" >&2
exit 66
fi
value=$(cat "$file")
export "$name=$value"
unset "${name}_FILE"
done
unset HAN_SECRET_VARS
exec "$@"
+50 -11
View File
@@ -5,39 +5,54 @@ services:
expose:
- "8000"
environment:
HAN_SECRET_VARS: >-
DATABASE_URL REDIS_URL REDIS_REALTIME_URL MESSAGE_SAFETY_SERVICE_TOKEN
BITRIX_LOCAL_APP_INTERNAL_TOKEN BITRIX_API_INBOX_TOKEN
KEYCLOAK_SETTINGS_BRIDGE_TOKEN SELECTEL_S3_ACCESS_KEY SELECTEL_S3_SECRET_KEY
CURSOR_HMAC_SECRET
DATABASE_URL_FILE: /run/secrets/api_database_url
REDIS_URL_FILE: /run/secrets/api_redis_url
REDIS_REALTIME_URL_FILE: /run/secrets/api_redis_realtime_url
MESSAGE_SAFETY_SERVICE_TOKEN_FILE: /run/secrets/message_safety_service_token
BITRIX_LOCAL_APP_INTERNAL_TOKEN_FILE: /run/secrets/bitrix_local_app_internal_token
BITRIX_API_INBOX_TOKEN_FILE: /run/secrets/bitrix_api_inbox_token
KEYCLOAK_SETTINGS_BRIDGE_TOKEN_FILE: /run/secrets/keycloak_settings_bridge_token
SELECTEL_S3_ACCESS_KEY_FILE: /run/secrets/selectel_s3_access_key
SELECTEL_S3_SECRET_KEY_FILE: /run/secrets/selectel_s3_secret_key
CURSOR_HMAC_SECRET_FILE: /run/secrets/cursor_hmac_secret
APP_ENV: ${APP_ENV}
API_PORT: ${API_PORT:-8000}
LOG_LEVEL: ${LOG_LEVEL:-INFO}
DATABASE_URL: ${DATABASE_URL}
REDIS_URL: ${REDIS_URL}
REDIS_REALTIME_URL: ${REDIS_REALTIME_URL}
KEYCLOAK_PUBLIC_URL: ${KEYCLOAK_PUBLIC_URL}
KEYCLOAK_INTERNAL_URL: ${KEYCLOAK_INTERNAL_URL}
KEYCLOAK_REALM: ${KEYCLOAK_REALM}
KEYCLOAK_AUDIENCE: ${KEYCLOAK_AUDIENCE}
MESSAGE_SAFETY_URL: ${MESSAGE_SAFETY_URL}
MESSAGE_SAFETY_SERVICE_TOKEN: ${MESSAGE_SAFETY_SERVICE_TOKEN}
MESSAGE_SAFETY_POST_TIMEOUT_SEC: ${MESSAGE_SAFETY_POST_TIMEOUT_SEC:-5}
MESSAGE_SAFETY_TASK_POLL_INTERVAL_SEC: ${MESSAGE_SAFETY_TASK_POLL_INTERVAL_SEC:-2}
MESSAGE_SAFETY_TASK_POLL_MAX_SEC: ${MESSAGE_SAFETY_TASK_POLL_MAX_SEC:-300}
MESSAGE_SAFETY_CIRCUIT_FAILURE_THRESHOLD: ${MESSAGE_SAFETY_CIRCUIT_FAILURE_THRESHOLD:-5}
MESSAGE_SAFETY_CIRCUIT_OPEN_SEC: ${MESSAGE_SAFETY_CIRCUIT_OPEN_SEC:-30}
BITRIX_LOCAL_APP_BASE_URL: ${BITRIX_LOCAL_APP_BASE_URL}
BITRIX_LOCAL_APP_INTERNAL_TOKEN: ${BITRIX_LOCAL_APP_INTERNAL_TOKEN}
BITRIX_API_INBOX_TOKEN: ${BITRIX_API_INBOX_TOKEN}
BITRIX_LOCAL_APP_HTTP_TIMEOUT_SEC: ${BITRIX_LOCAL_APP_HTTP_TIMEOUT_SEC:-20}
BITRIX_LOCAL_APP_CIRCUIT_FAILURE_THRESHOLD: ${BITRIX_LOCAL_APP_CIRCUIT_FAILURE_THRESHOLD:-5}
BITRIX_LOCAL_APP_CIRCUIT_OPEN_SEC: ${BITRIX_LOCAL_APP_CIRCUIT_OPEN_SEC:-30}
KEYCLOAK_SETTINGS_BRIDGE_TOKEN: ${KEYCLOAK_SETTINGS_BRIDGE_TOKEN}
SELECTEL_S3_ENDPOINT_URL: ${SELECTEL_S3_ENDPOINT_URL}
SELECTEL_S3_BUCKET_DOCUMENTS: ${SELECTEL_S3_BUCKET_DOCUMENTS}
SELECTEL_S3_BUCKET_ATTACHMENTS: ${SELECTEL_S3_BUCKET_ATTACHMENTS}
SELECTEL_S3_BUCKET_QUARANTINE: ${SELECTEL_S3_BUCKET_QUARANTINE}
SELECTEL_S3_ACCESS_KEY: ${SELECTEL_S3_ACCESS_KEY}
SELECTEL_S3_SECRET_KEY: ${SELECTEL_S3_SECRET_KEY}
OTEL_EXPORTER_OTLP_ENDPOINT: ${OTEL_EXPORTER_OTLP_ENDPOINT}
CURSOR_HMAC_SECRET: ${CURSOR_HMAC_SECRET}
NOTIFICATIONS_TOKEN_PRODUCER_TEST: ${NOTIFICATIONS_TOKEN_PRODUCER_TEST}
secrets:
- api_database_url
- api_redis_url
- api_redis_realtime_url
- message_safety_service_token
- bitrix_local_app_internal_token
- bitrix_api_inbox_token
- keycloak_settings_bridge_token
- selectel_s3_access_key
- selectel_s3_secret_key
- cursor_hmac_secret
healthcheck:
test:
- CMD
@@ -48,6 +63,8 @@ services:
timeout: 3s
retries: 5
restart: unless-stopped
ulimits:
core: {soft: 0, hard: 0}
networks:
- backend
- observability
@@ -55,3 +72,25 @@ services:
networks:
backend:
observability:
secrets:
api_database_url:
file: ${HAN_SECRETS_DIR:-/run/han-chat/secrets}/DATABASE_URL
api_redis_url:
file: ${HAN_SECRETS_DIR:-/run/han-chat/secrets}/REDIS_URL
api_redis_realtime_url:
file: ${HAN_SECRETS_DIR:-/run/han-chat/secrets}/REDIS_REALTIME_URL
message_safety_service_token:
file: ${HAN_SECRETS_DIR:-/run/han-chat/secrets}/MESSAGE_SAFETY_SERVICE_TOKEN
bitrix_local_app_internal_token:
file: ${HAN_SECRETS_DIR:-/run/han-chat/secrets}/BITRIX_LOCAL_APP_INTERNAL_TOKEN
bitrix_api_inbox_token:
file: ${HAN_SECRETS_DIR:-/run/han-chat/secrets}/BITRIX_API_INBOX_TOKEN
keycloak_settings_bridge_token:
file: ${HAN_SECRETS_DIR:-/run/han-chat/secrets}/KEYCLOAK_SETTINGS_BRIDGE_TOKEN
selectel_s3_access_key:
file: ${HAN_SECRETS_DIR:-/run/han-chat/secrets}/SELECTEL_S3_ACCESS_KEY
selectel_s3_secret_key:
file: ${HAN_SECRETS_DIR:-/run/han-chat/secrets}/SELECTEL_S3_SECRET_KEY
cursor_hmac_secret:
file: ${HAN_SECRETS_DIR:-/run/han-chat/secrets}/CURSOR_HMAC_SECRET
@@ -0,0 +1,31 @@
from app.logging_security import REDACTED, redact_event, sanitize_text
def test_redacts_sensitive_fields_recursively() -> None:
event = {
"authorization": "Bearer top-secret",
"nested": {
"database_url": "postgresql://user:password@db/app",
"safe": "kept",
},
}
redacted = redact_event(None, "info", event)
assert redacted["authorization"] == REDACTED
assert redacted["nested"]["database_url"] == REDACTED
assert redacted["nested"]["safe"] == "kept"
def test_redacts_credentials_embedded_in_text() -> None:
value = (
"POST https://callback-user:callback-password@example.test/cb"
"?token=query-secret Authorization=Bearer header-secret"
)
redacted = sanitize_text(value)
assert "callback-password" not in redacted
assert "query-secret" not in redacted
assert "header-secret" not in redacted
assert redacted.count(REDACTED) == 3