Правки различные
This commit is contained in:
@@ -89,8 +89,9 @@ IDGTL_SMS_CALLBACK_PUBLIC_URL=https://chat.example.ru/callbacks/idgtl/sms
|
||||
BITRIX_LOCAL_APP_BASE_URL=http://bitrix-local-app:8080
|
||||
BITRIX_API_INBOX_PATH=/internal/openlines/v1/inbox
|
||||
BITRIX_API_FORWARD_URL=http://api-backend:8000/internal/openlines/v1/inbox
|
||||
# Example only: set the actual VM2 private DNS name in the deployment .env.
|
||||
MESSAGE_SAFETY_URL=https://processing.internal:8443
|
||||
# Docker extra_hosts mapping for VM2 private listener: <hostname>=<private-ip>.
|
||||
# Docker extra_hosts mapping must use the same configured hostname: <hostname>=<private-ip>.
|
||||
MESSAGE_SAFETY_EXTRA_HOST=processing.internal=192.168.0.4
|
||||
MESSAGE_SAFETY_CA_HOST_PATH=/etc/han/ca/vm2-internal-ca.pem
|
||||
MESSAGE_SAFETY_API_PREFIX=/internal/safety/v2
|
||||
|
||||
@@ -59,8 +59,9 @@ han-notification-draft-cleanup-worker
|
||||
Токены генерируются `openssl rand -hex 32`. `MESSAGE_SAFETY_SERVICE_TOKEN`
|
||||
сохраняется как caller secret API backend; S3 credentials сервису Safety не
|
||||
передаются. В production подключение PostgreSQL должно использовать TLS. Target
|
||||
`MESSAGE_SAFETY_URL=https://processing.internal:8443`, API prefix
|
||||
`/internal/safety/v2`; certificate проверяется по CA из
|
||||
задаётся через `MESSAGE_SAFETY_URL=https://<private-vm2-name>:8443`
|
||||
(`processing.internal` — только пример), API prefix `/internal/safety/v2`;
|
||||
certificate проверяется по CA из
|
||||
`MESSAGE_SAFETY_CA_HOST_PATH`, plaintext HTTP запрещён.
|
||||
|
||||
Smoke-сценарий `producer_test`: отправить `POST
|
||||
|
||||
@@ -51,8 +51,10 @@ non-secret `PUBLIC_WEB_URL` environment variable. Keycloak resolves the
|
||||
initial `--import-realm`.
|
||||
|
||||
Use exact Expo universal/app links and web origins. Do not replace them with
|
||||
wildcards. `${PUBLIC_WEB_URL}/auth/callback` and `han-chat://auth/callback` are
|
||||
allow-listed by the initial realm import.
|
||||
wildcards. `${PUBLIC_WEB_URL}/auth/callback`, `${PUBLIC_WEB_URL}/mobile/oidc/callback`
|
||||
and `han-chat://auth/callback` are allow-listed by the initial realm import.
|
||||
Android Custom Tabs cannot follow a custom-scheme 302, so the mobile client uses
|
||||
the HTTPS bridge page and then opens `han-chat://auth/callback`.
|
||||
|
||||
The JDBC URL must use the managed PostgreSQL private endpoint, TLS verification and `currentSchema=keycloak`. The database role must have privileges only on schema `keycloak`.
|
||||
|
||||
|
||||
@@ -61,6 +61,7 @@
|
||||
"fullScopeAllowed": false,
|
||||
"redirectUris": [
|
||||
"${PUBLIC_WEB_URL}/auth/callback",
|
||||
"${PUBLIC_WEB_URL}/mobile/oidc/callback",
|
||||
"han-chat://auth/callback"
|
||||
],
|
||||
"webOrigins": [
|
||||
|
||||
+1
@@ -29,6 +29,7 @@ class RealmContractTest {
|
||||
assertTrue(realm.contains("\"optionalClientScopes\": [\"offline_access\"]"));
|
||||
assertTrue(realm.contains("\"han-chat://auth/callback\""));
|
||||
assertTrue(realm.contains("\"${PUBLIC_WEB_URL}/auth/callback\""));
|
||||
assertTrue(realm.contains("\"${PUBLIC_WEB_URL}/mobile/oidc/callback\""));
|
||||
assertTrue(realm.contains("\"${PUBLIC_WEB_URL}\""));
|
||||
assertFalse(realm.contains("chat.han0107.ru"));
|
||||
}
|
||||
|
||||
+1
-1
@@ -18,7 +18,7 @@ otpResend=Отправить код снова
|
||||
authBack=Назад
|
||||
verifyOtp=Подтвердить
|
||||
otpVerifying=Проверяем...
|
||||
otpSubmitUnavailable=Не удалось отправить код. Проверьте соединение и попробуйте ещё раз.
|
||||
otpSubmitUnavailable=Не удалось завершить вход. Если код уже принят, закройте окно и откройте приложение.
|
||||
mockMode=Тестовый режим отправки кода
|
||||
phoneInvalid=Проверьте формат номера телефона.
|
||||
otpInvalid=Код неверен, истёк или уже использован.
|
||||
|
||||
@@ -7,9 +7,10 @@ RUN apt-get update \
|
||||
COPY nginx.conf.template /etc/nginx/templates-src/nginx.conf.template
|
||||
COPY templates /etc/nginx/templates-src/sites
|
||||
COPY snippets /etc/nginx/snippets
|
||||
COPY static /etc/nginx/static
|
||||
COPY scripts/entrypoint.sh /usr/local/bin/han-nginx-entrypoint
|
||||
RUN sed -i 's/\r$//' /usr/local/bin/han-nginx-entrypoint \
|
||||
&& /bin/sh -n /usr/local/bin/han-nginx-entrypoint \
|
||||
&& chmod 0555 /usr/local/bin/han-nginx-entrypoint \
|
||||
&& find /etc/nginx/templates-src /etc/nginx/snippets -type f -exec chmod 0444 {} +
|
||||
&& find /etc/nginx/templates-src /etc/nginx/snippets /etc/nginx/static -type f -exec chmod 0444 {} +
|
||||
ENTRYPOINT ["/usr/local/bin/han-nginx-entrypoint"]
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||
<title>HAN Chat</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 16px;
|
||||
padding: 32px 24px;
|
||||
background: #fff;
|
||||
color: #252525;
|
||||
font-family: Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
text-align: center;
|
||||
}
|
||||
p { margin: 0; line-height: 1.5; }
|
||||
a {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 220px;
|
||||
height: 56px;
|
||||
padding: 0 24px;
|
||||
border-radius: 12px;
|
||||
background: #030213;
|
||||
color: #fff;
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
text-decoration: none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<p>Вход выполнен. Возвращаем в приложение…</p>
|
||||
<p>
|
||||
<a id="han-open-app" href="han-chat://auth/callbackHAN_CALLBACK_QUERY">Открыть HAN Chat</a>
|
||||
</p>
|
||||
<script src="/mobile/oidc/callback.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,6 @@
|
||||
(function () {
|
||||
var target = "han-chat://auth/callback" + window.location.search + window.location.hash;
|
||||
var link = document.getElementById("han-open-app");
|
||||
if (link) link.href = target;
|
||||
window.location.replace(target);
|
||||
})();
|
||||
@@ -139,6 +139,23 @@ server {
|
||||
add_header Cache-Control "no-cache";
|
||||
include /etc/nginx/generated/security-headers.conf;
|
||||
}
|
||||
location = /mobile/oidc/callback {
|
||||
default_type text/html;
|
||||
charset utf-8;
|
||||
alias /etc/nginx/static/mobile-oidc-callback.html;
|
||||
sub_filter_once on;
|
||||
sub_filter_types text/html;
|
||||
sub_filter HAN_CALLBACK_QUERY ?$args;
|
||||
add_header Cache-Control "no-store" always;
|
||||
include /etc/nginx/generated/security-headers.conf;
|
||||
}
|
||||
location = /mobile/oidc/callback.js {
|
||||
default_type text/javascript;
|
||||
charset utf-8;
|
||||
alias /etc/nginx/static/mobile-oidc-callback.js;
|
||||
add_header Cache-Control "no-store" always;
|
||||
include /etc/nginx/generated/security-headers.conf;
|
||||
}
|
||||
location ^~ /auth/resources/ {
|
||||
include /etc/nginx/snippets/proxy-keycloak.conf;
|
||||
proxy_pass http://keycloak_upstream;
|
||||
|
||||
@@ -197,14 +197,29 @@ def validate_shared(env: dict[str, str], errors: list[str]) -> None:
|
||||
errors.append(f"{key}: ожидается http URL с Docker DNS service name")
|
||||
if not env.get("KEYCLOAK_INTERNAL_URL", "").rstrip("/").endswith("/auth"):
|
||||
errors.append("KEYCLOAK_INTERNAL_URL: внутренний URL должен заканчиваться на /auth")
|
||||
if env.get("MESSAGE_SAFETY_URL", "").rstrip("/") != "https://processing.internal:8443":
|
||||
safety_url = env.get("MESSAGE_SAFETY_URL", "").rstrip("/")
|
||||
parsed_safety_url = urlparse(safety_url)
|
||||
try:
|
||||
safety_port = parsed_safety_url.port
|
||||
except ValueError:
|
||||
safety_port = None
|
||||
safety_host = parsed_safety_url.hostname or ""
|
||||
if (
|
||||
parsed_safety_url.scheme != "https"
|
||||
or safety_port != 8443
|
||||
or not re.fullmatch(r"[A-Za-z0-9.-]+", safety_host)
|
||||
or safety_host.lower() in {"message-safety", "localhost", "127.0.0.1"}
|
||||
or parsed_safety_url.path
|
||||
or parsed_safety_url.params
|
||||
or parsed_safety_url.query
|
||||
or parsed_safety_url.fragment
|
||||
):
|
||||
errors.append(
|
||||
"MESSAGE_SAFETY_URL: ожидается remote TLS endpoint "
|
||||
"https://processing.internal:8443"
|
||||
"https://<private VM2 hostname>:8443, не local/stub service"
|
||||
)
|
||||
try:
|
||||
extra_host, extra_ip = env.get("MESSAGE_SAFETY_EXTRA_HOST", "").rsplit("=", 1)
|
||||
safety_host = urlparse(env.get("MESSAGE_SAFETY_URL", "")).hostname
|
||||
address = ipaddress.ip_address(extra_ip)
|
||||
if extra_host != safety_host or not address.is_private:
|
||||
raise ValueError
|
||||
|
||||
@@ -152,6 +152,28 @@ class SecretHygieneTests(unittest.TestCase):
|
||||
self.assertNotEqual(result.returncode, 0)
|
||||
self.assertIn(expected_error, result.stderr)
|
||||
|
||||
def test_validator_accepts_configured_private_safety_hostname(self) -> None:
|
||||
example = (ROOT / ".env.example").read_text(encoding="utf-8")
|
||||
configured = (
|
||||
example.replace(
|
||||
"KEYCLOAK_OTP_MOCK_RISK_ACCEPTED=false",
|
||||
"KEYCLOAK_OTP_MOCK_RISK_ACCEPTED=true",
|
||||
)
|
||||
.replace(
|
||||
"MESSAGE_SAFETY_URL=https://processing.internal:8443",
|
||||
"MESSAGE_SAFETY_URL=https://safety.vm2.corp.internal:8443",
|
||||
)
|
||||
.replace(
|
||||
"MESSAGE_SAFETY_EXTRA_HOST=processing.internal=192.168.0.4",
|
||||
"MESSAGE_SAFETY_EXTRA_HOST=safety.vm2.corp.internal=192.168.0.4",
|
||||
)
|
||||
)
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
config = Path(directory) / ".env"
|
||||
config.write_text(configured, encoding="utf-8")
|
||||
result = self.run_validator(config)
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -1085,7 +1085,7 @@ Runtime refresh: poll `MAX(updated_at)` каждые 30 секунд; новый
|
||||
|
||||
`SELECTEL_S3_QUARANTINE_READ_*` принадлежит `message-safety`, не должен передаваться контейнеру API. Новые env сначала документируются в arch-04.
|
||||
|
||||
В production validator принимает только remote `https://<private-vm2-name>:8443`, требует читаемый `MESSAGE_SAFETY_CA_FILE`, отклоняет plaintext и cross-host Docker hostname. Host bind CA задаётся runbook-переменной `MESSAGE_SAFETY_CA_HOST_PATH`; runtime использует только container path `MESSAGE_SAFETY_CA_FILE`.
|
||||
В production validator принимает remote `https://<private-vm2-name>:8443`, требует читаемый `MESSAGE_SAFETY_CA_FILE`, отклоняет plaintext и local/stub Docker hostname. Конкретное private DNS-имя ВМ2 определяется внутренним доменом окружения и задаётся в `MESSAGE_SAFETY_URL` и согласованном `MESSAGE_SAFETY_EXTRA_HOST`; каноническое имя не требуется. Host bind CA задаётся runbook-переменной `MESSAGE_SAFETY_CA_HOST_PATH`; runtime использует только container path `MESSAGE_SAFETY_CA_FILE`.
|
||||
|
||||
## 19. Rate limiting
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ Nginx ВМ1 — публичная точка входа приложения: f
|
||||
|---|---|---|
|
||||
| `/api/` | `api-backend:8000` | REST; `/api/v1/realtime` WS |
|
||||
| `/auth/` | `keycloak:8080` | OIDC/OTP, prefix/hostname согласован с issuer |
|
||||
| exact `/mobile/oidc/callback` | static nginx | HTTPS-мост Android Custom Tabs → `han-chat://auth/callback` |
|
||||
| exact `/callbacks/idgtl/sms` | `sms-service:8080` | public HTTPS POST Direct; IP allowlist + Basic auth в upstream |
|
||||
| `/bitrix/handler`, `/bitrix/install`, `/bitrix/placement` | `bitrix-local-app:8080` | public HTTPS |
|
||||
| exact `/health/live`, `/health/ready` | `bitrix-local-app:8080` | по умолчанию не публикуются; только при явно выбранной ops/monitoring policy |
|
||||
|
||||
@@ -92,6 +92,7 @@ han-chat-frontend
|
||||
|
||||
```text
|
||||
https://tohin.ru/auth/callback
|
||||
https://tohin.ru/mobile/oidc/callback
|
||||
han-chat://auth/callback
|
||||
<Expo native scheme/callback, exact value после сборки>
|
||||
```
|
||||
|
||||
@@ -49,7 +49,9 @@ webhooks собственного host. Compose, IAM principal и secret bundle
|
||||
performance, egress и rollback gates своего runbook.
|
||||
2. ВМ1 использует
|
||||
`MESSAGE_SAFETY_URL=https://<VM2_PRIVATE_DNS>:8443` и root-owned
|
||||
`MESSAGE_SAFETY_CA_HOST_PATH`.
|
||||
`MESSAGE_SAFETY_CA_HOST_PATH`. Значение `<VM2_PRIVATE_DNS>` определяется
|
||||
внутренним DNS-доменом окружения и задаётся в `.env`; фиксированное
|
||||
каноническое имя не требуется.
|
||||
3. Internal CA читается фактическим UID API и не читается посторонним UID.
|
||||
4. Service token paired, private route/SG разрешают `8443` только от ВМ1/ops.
|
||||
5. Local `message-safety`, Redis DB2, local Safety rules env и stub fallback
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user