diff --git a/.gitattributes b/.gitattributes index 886d9ff..38a2532 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,3 +1,3 @@ *.sh text eol=lf -codebase/services/deployment/secrets/han-compose text eol=lf -codebase/services/deployment/han-message-safety-mode text eol=lf +VM2_services/codebase/services/deployment/secrets/han-compose text eol=lf +VM2_services/codebase/services/deployment/han-message-safety-mode text eol=lf diff --git a/codebase/.gitignore b/VM1_app/codebase/.gitignore similarity index 100% rename from codebase/.gitignore rename to VM1_app/codebase/.gitignore diff --git a/codebase/backend/.env.example b/VM1_app/codebase/backend/.env.example similarity index 100% rename from codebase/backend/.env.example rename to VM1_app/codebase/backend/.env.example diff --git a/codebase/backend/.gitattributes b/VM1_app/codebase/backend/.gitattributes similarity index 100% rename from codebase/backend/.gitattributes rename to VM1_app/codebase/backend/.gitattributes diff --git a/codebase/backend/.gitignore b/VM1_app/codebase/backend/.gitignore similarity index 100% rename from codebase/backend/.gitignore rename to VM1_app/codebase/backend/.gitignore diff --git a/codebase/backend/api-backend/.dockerignore b/VM1_app/codebase/backend/api-backend/.dockerignore similarity index 100% rename from codebase/backend/api-backend/.dockerignore rename to VM1_app/codebase/backend/api-backend/.dockerignore diff --git a/codebase/backend/api-backend/Dockerfile b/VM1_app/codebase/backend/api-backend/Dockerfile similarity index 100% rename from codebase/backend/api-backend/Dockerfile rename to VM1_app/codebase/backend/api-backend/Dockerfile diff --git a/codebase/backend/api-backend/README.md b/VM1_app/codebase/backend/api-backend/README.md similarity index 100% rename from codebase/backend/api-backend/README.md rename to VM1_app/codebase/backend/api-backend/README.md diff --git a/codebase/backend/api-backend/alembic.ini b/VM1_app/codebase/backend/api-backend/alembic.ini similarity index 100% rename from codebase/backend/api-backend/alembic.ini rename to VM1_app/codebase/backend/api-backend/alembic.ini diff --git a/codebase/backend/api-backend/alembic/env.py b/VM1_app/codebase/backend/api-backend/alembic/env.py similarity index 100% rename from codebase/backend/api-backend/alembic/env.py rename to VM1_app/codebase/backend/api-backend/alembic/env.py diff --git a/codebase/backend/api-backend/alembic/versions/0001_initial_han_app.py b/VM1_app/codebase/backend/api-backend/alembic/versions/0001_initial_han_app.py similarity index 100% rename from codebase/backend/api-backend/alembic/versions/0001_initial_han_app.py rename to VM1_app/codebase/backend/api-backend/alembic/versions/0001_initial_han_app.py diff --git a/codebase/backend/api-backend/alembic/versions/0002_qualify_pgcrypto_digest.py b/VM1_app/codebase/backend/api-backend/alembic/versions/0002_qualify_pgcrypto_digest.py similarity index 100% rename from codebase/backend/api-backend/alembic/versions/0002_qualify_pgcrypto_digest.py rename to VM1_app/codebase/backend/api-backend/alembic/versions/0002_qualify_pgcrypto_digest.py diff --git a/codebase/backend/api-backend/alembic/versions/0003_consent_device_audit_context.py b/VM1_app/codebase/backend/api-backend/alembic/versions/0003_consent_device_audit_context.py similarity index 100% rename from codebase/backend/api-backend/alembic/versions/0003_consent_device_audit_context.py rename to VM1_app/codebase/backend/api-backend/alembic/versions/0003_consent_device_audit_context.py diff --git a/codebase/backend/api-backend/alembic/versions/0004_device_and_otp_limits.py b/VM1_app/codebase/backend/api-backend/alembic/versions/0004_device_and_otp_limits.py similarity index 100% rename from codebase/backend/api-backend/alembic/versions/0004_device_and_otp_limits.py rename to VM1_app/codebase/backend/api-backend/alembic/versions/0004_device_and_otp_limits.py diff --git a/codebase/backend/api-backend/alembic/versions/0005_otp_runtime_settings.py b/VM1_app/codebase/backend/api-backend/alembic/versions/0005_otp_runtime_settings.py similarity index 100% rename from codebase/backend/api-backend/alembic/versions/0005_otp_runtime_settings.py rename to VM1_app/codebase/backend/api-backend/alembic/versions/0005_otp_runtime_settings.py diff --git a/codebase/backend/api-backend/alembic/versions/0006_consent_privacy_policy_url.py b/VM1_app/codebase/backend/api-backend/alembic/versions/0006_consent_privacy_policy_url.py similarity index 100% rename from codebase/backend/api-backend/alembic/versions/0006_consent_privacy_policy_url.py rename to VM1_app/codebase/backend/api-backend/alembic/versions/0006_consent_privacy_policy_url.py diff --git a/codebase/backend/api-backend/alembic/versions/0007_consent_marketing_document_url.py b/VM1_app/codebase/backend/api-backend/alembic/versions/0007_consent_marketing_document_url.py similarity index 100% rename from codebase/backend/api-backend/alembic/versions/0007_consent_marketing_document_url.py rename to VM1_app/codebase/backend/api-backend/alembic/versions/0007_consent_marketing_document_url.py diff --git a/codebase/backend/api-backend/alembic/versions/0008_notification_center_v1.py b/VM1_app/codebase/backend/api-backend/alembic/versions/0008_notification_center_v1.py similarity index 100% rename from codebase/backend/api-backend/alembic/versions/0008_notification_center_v1.py rename to VM1_app/codebase/backend/api-backend/alembic/versions/0008_notification_center_v1.py diff --git a/codebase/backend/api-backend/alembic/versions/0009_chat_message_max_length.py b/VM1_app/codebase/backend/api-backend/alembic/versions/0009_chat_message_max_length.py similarity index 100% rename from codebase/backend/api-backend/alembic/versions/0009_chat_message_max_length.py rename to VM1_app/codebase/backend/api-backend/alembic/versions/0009_chat_message_max_length.py diff --git a/codebase/backend/api-backend/alembic/versions/0010_contact_map_dedup.py b/VM1_app/codebase/backend/api-backend/alembic/versions/0010_contact_map_dedup.py similarity index 100% rename from codebase/backend/api-backend/alembic/versions/0010_contact_map_dedup.py rename to VM1_app/codebase/backend/api-backend/alembic/versions/0010_contact_map_dedup.py diff --git a/codebase/backend/api-backend/alembic/versions/0011_module07_contract.py b/VM1_app/codebase/backend/api-backend/alembic/versions/0011_module07_contract.py similarity index 100% rename from codebase/backend/api-backend/alembic/versions/0011_module07_contract.py rename to VM1_app/codebase/backend/api-backend/alembic/versions/0011_module07_contract.py diff --git a/codebase/backend/api-backend/alembic/versions/0012_safety_v2_checkpoint.py b/VM1_app/codebase/backend/api-backend/alembic/versions/0012_safety_v2_checkpoint.py similarity index 100% rename from codebase/backend/api-backend/alembic/versions/0012_safety_v2_checkpoint.py rename to VM1_app/codebase/backend/api-backend/alembic/versions/0012_safety_v2_checkpoint.py diff --git a/codebase/backend/api-backend/app/__init__.py b/VM1_app/codebase/backend/api-backend/app/__init__.py similarity index 100% rename from codebase/backend/api-backend/app/__init__.py rename to VM1_app/codebase/backend/api-backend/app/__init__.py diff --git a/codebase/backend/api-backend/app/auth.py b/VM1_app/codebase/backend/api-backend/app/auth.py similarity index 100% rename from codebase/backend/api-backend/app/auth.py rename to VM1_app/codebase/backend/api-backend/app/auth.py diff --git a/codebase/backend/api-backend/app/chat_settings.py b/VM1_app/codebase/backend/api-backend/app/chat_settings.py similarity index 100% rename from codebase/backend/api-backend/app/chat_settings.py rename to VM1_app/codebase/backend/api-backend/app/chat_settings.py diff --git a/codebase/backend/api-backend/app/cli/__init__.py b/VM1_app/codebase/backend/api-backend/app/cli/__init__.py similarity index 100% rename from codebase/backend/api-backend/app/cli/__init__.py rename to VM1_app/codebase/backend/api-backend/app/cli/__init__.py diff --git a/codebase/backend/api-backend/app/cli/seed_settings.py b/VM1_app/codebase/backend/api-backend/app/cli/seed_settings.py similarity index 100% rename from codebase/backend/api-backend/app/cli/seed_settings.py rename to VM1_app/codebase/backend/api-backend/app/cli/seed_settings.py diff --git a/codebase/backend/api-backend/app/cli/validate_settings.py b/VM1_app/codebase/backend/api-backend/app/cli/validate_settings.py similarity index 100% rename from codebase/backend/api-backend/app/cli/validate_settings.py rename to VM1_app/codebase/backend/api-backend/app/cli/validate_settings.py diff --git a/codebase/backend/api-backend/app/db.py b/VM1_app/codebase/backend/api-backend/app/db.py similarity index 100% rename from codebase/backend/api-backend/app/db.py rename to VM1_app/codebase/backend/api-backend/app/db.py diff --git a/codebase/backend/api-backend/app/integrations.py b/VM1_app/codebase/backend/api-backend/app/integrations.py similarity index 100% rename from codebase/backend/api-backend/app/integrations.py rename to VM1_app/codebase/backend/api-backend/app/integrations.py diff --git a/codebase/backend/api-backend/app/logging_security.py b/VM1_app/codebase/backend/api-backend/app/logging_security.py similarity index 100% rename from codebase/backend/api-backend/app/logging_security.py rename to VM1_app/codebase/backend/api-backend/app/logging_security.py diff --git a/codebase/backend/api-backend/app/main.py b/VM1_app/codebase/backend/api-backend/app/main.py similarity index 100% rename from codebase/backend/api-backend/app/main.py rename to VM1_app/codebase/backend/api-backend/app/main.py diff --git a/codebase/backend/api-backend/app/metrics.py b/VM1_app/codebase/backend/api-backend/app/metrics.py similarity index 100% rename from codebase/backend/api-backend/app/metrics.py rename to VM1_app/codebase/backend/api-backend/app/metrics.py diff --git a/codebase/backend/api-backend/app/notification_models.py b/VM1_app/codebase/backend/api-backend/app/notification_models.py similarity index 100% rename from codebase/backend/api-backend/app/notification_models.py rename to VM1_app/codebase/backend/api-backend/app/notification_models.py diff --git a/codebase/backend/api-backend/app/notification_routes.py b/VM1_app/codebase/backend/api-backend/app/notification_routes.py similarity index 100% rename from codebase/backend/api-backend/app/notification_routes.py rename to VM1_app/codebase/backend/api-backend/app/notification_routes.py diff --git a/codebase/backend/api-backend/app/notification_schemas.py b/VM1_app/codebase/backend/api-backend/app/notification_schemas.py similarity index 100% rename from codebase/backend/api-backend/app/notification_schemas.py rename to VM1_app/codebase/backend/api-backend/app/notification_schemas.py diff --git a/codebase/backend/api-backend/app/notification_service.py b/VM1_app/codebase/backend/api-backend/app/notification_service.py similarity index 100% rename from codebase/backend/api-backend/app/notification_service.py rename to VM1_app/codebase/backend/api-backend/app/notification_service.py diff --git a/codebase/backend/api-backend/app/otp_settings.py b/VM1_app/codebase/backend/api-backend/app/otp_settings.py similarity index 100% rename from codebase/backend/api-backend/app/otp_settings.py rename to VM1_app/codebase/backend/api-backend/app/otp_settings.py diff --git a/codebase/backend/api-backend/app/postgres.py b/VM1_app/codebase/backend/api-backend/app/postgres.py similarity index 100% rename from codebase/backend/api-backend/app/postgres.py rename to VM1_app/codebase/backend/api-backend/app/postgres.py diff --git a/codebase/backend/api-backend/app/realtime.py b/VM1_app/codebase/backend/api-backend/app/realtime.py similarity index 100% rename from codebase/backend/api-backend/app/realtime.py rename to VM1_app/codebase/backend/api-backend/app/realtime.py diff --git a/codebase/backend/api-backend/app/schemas.py b/VM1_app/codebase/backend/api-backend/app/schemas.py similarity index 100% rename from codebase/backend/api-backend/app/schemas.py rename to VM1_app/codebase/backend/api-backend/app/schemas.py diff --git a/codebase/backend/api-backend/app/services.py b/VM1_app/codebase/backend/api-backend/app/services.py similarity index 100% rename from codebase/backend/api-backend/app/services.py rename to VM1_app/codebase/backend/api-backend/app/services.py diff --git a/codebase/backend/api-backend/app/settings.py b/VM1_app/codebase/backend/api-backend/app/settings.py similarity index 100% rename from codebase/backend/api-backend/app/settings.py rename to VM1_app/codebase/backend/api-backend/app/settings.py diff --git a/codebase/backend/api-backend/app/telemetry.py b/VM1_app/codebase/backend/api-backend/app/telemetry.py similarity index 100% rename from codebase/backend/api-backend/app/telemetry.py rename to VM1_app/codebase/backend/api-backend/app/telemetry.py diff --git a/codebase/backend/api-backend/app/workers.py b/VM1_app/codebase/backend/api-backend/app/workers.py similarity index 100% rename from codebase/backend/api-backend/app/workers.py rename to VM1_app/codebase/backend/api-backend/app/workers.py diff --git a/codebase/backend/api-backend/container-entrypoint.sh b/VM1_app/codebase/backend/api-backend/container-entrypoint.sh similarity index 100% rename from codebase/backend/api-backend/container-entrypoint.sh rename to VM1_app/codebase/backend/api-backend/container-entrypoint.sh diff --git a/codebase/backend/api-backend/docker-compose.yml b/VM1_app/codebase/backend/api-backend/docker-compose.yml similarity index 100% rename from codebase/backend/api-backend/docker-compose.yml rename to VM1_app/codebase/backend/api-backend/docker-compose.yml diff --git a/codebase/backend/api-backend/openapi.yaml b/VM1_app/codebase/backend/api-backend/openapi.yaml similarity index 100% rename from codebase/backend/api-backend/openapi.yaml rename to VM1_app/codebase/backend/api-backend/openapi.yaml diff --git a/codebase/backend/api-backend/pyproject.toml b/VM1_app/codebase/backend/api-backend/pyproject.toml similarity index 100% rename from codebase/backend/api-backend/pyproject.toml rename to VM1_app/codebase/backend/api-backend/pyproject.toml diff --git a/codebase/backend/api-backend/tests/contract/test_clients.py b/VM1_app/codebase/backend/api-backend/tests/contract/test_clients.py similarity index 100% rename from codebase/backend/api-backend/tests/contract/test_clients.py rename to VM1_app/codebase/backend/api-backend/tests/contract/test_clients.py diff --git a/codebase/backend/api-backend/tests/contract/test_openapi.py b/VM1_app/codebase/backend/api-backend/tests/contract/test_openapi.py similarity index 100% rename from codebase/backend/api-backend/tests/contract/test_openapi.py rename to VM1_app/codebase/backend/api-backend/tests/contract/test_openapi.py diff --git a/codebase/backend/api-backend/tests/unit/test_audit_context.py b/VM1_app/codebase/backend/api-backend/tests/unit/test_audit_context.py similarity index 100% rename from codebase/backend/api-backend/tests/unit/test_audit_context.py rename to VM1_app/codebase/backend/api-backend/tests/unit/test_audit_context.py diff --git a/codebase/backend/api-backend/tests/unit/test_cli_settings.py b/VM1_app/codebase/backend/api-backend/tests/unit/test_cli_settings.py similarity index 100% rename from codebase/backend/api-backend/tests/unit/test_cli_settings.py rename to VM1_app/codebase/backend/api-backend/tests/unit/test_cli_settings.py diff --git a/codebase/backend/api-backend/tests/unit/test_domain.py b/VM1_app/codebase/backend/api-backend/tests/unit/test_domain.py similarity index 100% rename from codebase/backend/api-backend/tests/unit/test_domain.py rename to VM1_app/codebase/backend/api-backend/tests/unit/test_domain.py diff --git a/codebase/backend/api-backend/tests/unit/test_logging_security.py b/VM1_app/codebase/backend/api-backend/tests/unit/test_logging_security.py similarity index 100% rename from codebase/backend/api-backend/tests/unit/test_logging_security.py rename to VM1_app/codebase/backend/api-backend/tests/unit/test_logging_security.py diff --git a/codebase/backend/api-backend/tests/unit/test_notifications.py b/VM1_app/codebase/backend/api-backend/tests/unit/test_notifications.py similarity index 100% rename from codebase/backend/api-backend/tests/unit/test_notifications.py rename to VM1_app/codebase/backend/api-backend/tests/unit/test_notifications.py diff --git a/codebase/backend/api-backend/tests/unit/test_telemetry.py b/VM1_app/codebase/backend/api-backend/tests/unit/test_telemetry.py similarity index 100% rename from codebase/backend/api-backend/tests/unit/test_telemetry.py rename to VM1_app/codebase/backend/api-backend/tests/unit/test_telemetry.py diff --git a/codebase/backend/bitrix-local-app/Dockerfile b/VM1_app/codebase/backend/bitrix-local-app/Dockerfile similarity index 100% rename from codebase/backend/bitrix-local-app/Dockerfile rename to VM1_app/codebase/backend/bitrix-local-app/Dockerfile diff --git a/codebase/backend/bitrix-local-app/alembic.ini b/VM1_app/codebase/backend/bitrix-local-app/alembic.ini similarity index 100% rename from codebase/backend/bitrix-local-app/alembic.ini rename to VM1_app/codebase/backend/bitrix-local-app/alembic.ini diff --git a/codebase/backend/bitrix-local-app/alembic/env.py b/VM1_app/codebase/backend/bitrix-local-app/alembic/env.py similarity index 100% rename from codebase/backend/bitrix-local-app/alembic/env.py rename to VM1_app/codebase/backend/bitrix-local-app/alembic/env.py diff --git a/codebase/backend/bitrix-local-app/alembic/versions/0001_bitrix_local_schema.py b/VM1_app/codebase/backend/bitrix-local-app/alembic/versions/0001_bitrix_local_schema.py similarity index 100% rename from codebase/backend/bitrix-local-app/alembic/versions/0001_bitrix_local_schema.py rename to VM1_app/codebase/backend/bitrix-local-app/alembic/versions/0001_bitrix_local_schema.py diff --git a/codebase/backend/bitrix-local-app/app/__init__.py b/VM1_app/codebase/backend/bitrix-local-app/app/__init__.py similarity index 100% rename from codebase/backend/bitrix-local-app/app/__init__.py rename to VM1_app/codebase/backend/bitrix-local-app/app/__init__.py diff --git a/codebase/backend/bitrix-local-app/app/main.py b/VM1_app/codebase/backend/bitrix-local-app/app/main.py similarity index 100% rename from codebase/backend/bitrix-local-app/app/main.py rename to VM1_app/codebase/backend/bitrix-local-app/app/main.py diff --git a/codebase/backend/bitrix-local-app/app/models.py b/VM1_app/codebase/backend/bitrix-local-app/app/models.py similarity index 100% rename from codebase/backend/bitrix-local-app/app/models.py rename to VM1_app/codebase/backend/bitrix-local-app/app/models.py diff --git a/codebase/backend/bitrix-local-app/app/postgres.py b/VM1_app/codebase/backend/bitrix-local-app/app/postgres.py similarity index 100% rename from codebase/backend/bitrix-local-app/app/postgres.py rename to VM1_app/codebase/backend/bitrix-local-app/app/postgres.py diff --git a/codebase/backend/bitrix-local-app/container-entrypoint.sh b/VM1_app/codebase/backend/bitrix-local-app/container-entrypoint.sh similarity index 100% rename from codebase/backend/bitrix-local-app/container-entrypoint.sh rename to VM1_app/codebase/backend/bitrix-local-app/container-entrypoint.sh diff --git a/codebase/backend/bitrix-local-app/openapi.yaml b/VM1_app/codebase/backend/bitrix-local-app/openapi.yaml similarity index 100% rename from codebase/backend/bitrix-local-app/openapi.yaml rename to VM1_app/codebase/backend/bitrix-local-app/openapi.yaml diff --git a/codebase/backend/bitrix-local-app/pyproject.toml b/VM1_app/codebase/backend/bitrix-local-app/pyproject.toml similarity index 100% rename from codebase/backend/bitrix-local-app/pyproject.toml rename to VM1_app/codebase/backend/bitrix-local-app/pyproject.toml diff --git a/codebase/backend/bitrix-local-app/tests/test_core.py b/VM1_app/codebase/backend/bitrix-local-app/tests/test_core.py similarity index 100% rename from codebase/backend/bitrix-local-app/tests/test_core.py rename to VM1_app/codebase/backend/bitrix-local-app/tests/test_core.py diff --git a/codebase/backend/bitrix-sync/Dockerfile b/VM1_app/codebase/backend/bitrix-sync/Dockerfile similarity index 100% rename from codebase/backend/bitrix-sync/Dockerfile rename to VM1_app/codebase/backend/bitrix-sync/Dockerfile diff --git a/codebase/backend/bitrix-sync/alembic.ini b/VM1_app/codebase/backend/bitrix-sync/alembic.ini similarity index 100% rename from codebase/backend/bitrix-sync/alembic.ini rename to VM1_app/codebase/backend/bitrix-sync/alembic.ini diff --git a/codebase/backend/bitrix-sync/alembic/env.py b/VM1_app/codebase/backend/bitrix-sync/alembic/env.py similarity index 100% rename from codebase/backend/bitrix-sync/alembic/env.py rename to VM1_app/codebase/backend/bitrix-sync/alembic/env.py diff --git a/codebase/backend/bitrix-sync/alembic/versions/0001_connectivity_stub_baseline.py b/VM1_app/codebase/backend/bitrix-sync/alembic/versions/0001_connectivity_stub_baseline.py similarity index 100% rename from codebase/backend/bitrix-sync/alembic/versions/0001_connectivity_stub_baseline.py rename to VM1_app/codebase/backend/bitrix-sync/alembic/versions/0001_connectivity_stub_baseline.py diff --git a/codebase/backend/bitrix-sync/app/__init__.py b/VM1_app/codebase/backend/bitrix-sync/app/__init__.py similarity index 100% rename from codebase/backend/bitrix-sync/app/__init__.py rename to VM1_app/codebase/backend/bitrix-sync/app/__init__.py diff --git a/codebase/backend/bitrix-sync/app/main.py b/VM1_app/codebase/backend/bitrix-sync/app/main.py similarity index 100% rename from codebase/backend/bitrix-sync/app/main.py rename to VM1_app/codebase/backend/bitrix-sync/app/main.py diff --git a/codebase/backend/bitrix-sync/app/postgres.py b/VM1_app/codebase/backend/bitrix-sync/app/postgres.py similarity index 100% rename from codebase/backend/bitrix-sync/app/postgres.py rename to VM1_app/codebase/backend/bitrix-sync/app/postgres.py diff --git a/codebase/backend/bitrix-sync/container-entrypoint.sh b/VM1_app/codebase/backend/bitrix-sync/container-entrypoint.sh similarity index 100% rename from codebase/backend/bitrix-sync/container-entrypoint.sh rename to VM1_app/codebase/backend/bitrix-sync/container-entrypoint.sh diff --git a/codebase/backend/bitrix-sync/openapi.yaml b/VM1_app/codebase/backend/bitrix-sync/openapi.yaml similarity index 100% rename from codebase/backend/bitrix-sync/openapi.yaml rename to VM1_app/codebase/backend/bitrix-sync/openapi.yaml diff --git a/codebase/backend/bitrix-sync/pyproject.toml b/VM1_app/codebase/backend/bitrix-sync/pyproject.toml similarity index 100% rename from codebase/backend/bitrix-sync/pyproject.toml rename to VM1_app/codebase/backend/bitrix-sync/pyproject.toml diff --git a/codebase/backend/bitrix-sync/tests/test_service.py b/VM1_app/codebase/backend/bitrix-sync/tests/test_service.py similarity index 100% rename from codebase/backend/bitrix-sync/tests/test_service.py rename to VM1_app/codebase/backend/bitrix-sync/tests/test_service.py diff --git a/codebase/backend/deployment/DEPLOYMENT_GUIDE.ru.md b/VM1_app/codebase/backend/deployment/DEPLOYMENT_GUIDE.ru.md similarity index 100% rename from codebase/backend/deployment/DEPLOYMENT_GUIDE.ru.md rename to VM1_app/codebase/backend/deployment/DEPLOYMENT_GUIDE.ru.md diff --git a/codebase/backend/deployment/RUNBOOK.md b/VM1_app/codebase/backend/deployment/RUNBOOK.md similarity index 100% rename from codebase/backend/deployment/RUNBOOK.md rename to VM1_app/codebase/backend/deployment/RUNBOOK.md diff --git a/codebase/backend/deployment/RUNBOOK.ru.md b/VM1_app/codebase/backend/deployment/RUNBOOK.ru.md similarity index 100% rename from codebase/backend/deployment/RUNBOOK.ru.md rename to VM1_app/codebase/backend/deployment/RUNBOOK.ru.md diff --git a/codebase/backend/deployment/app-settings.production-like.yaml b/VM1_app/codebase/backend/deployment/app-settings.production-like.yaml similarity index 100% rename from codebase/backend/deployment/app-settings.production-like.yaml rename to VM1_app/codebase/backend/deployment/app-settings.production-like.yaml diff --git a/codebase/backend/deployment/docker-compose.jobs.yml b/VM1_app/codebase/backend/deployment/docker-compose.jobs.yml similarity index 100% rename from codebase/backend/deployment/docker-compose.jobs.yml rename to VM1_app/codebase/backend/deployment/docker-compose.jobs.yml diff --git a/codebase/backend/deployment/scripts/backup.sh b/VM1_app/codebase/backend/deployment/scripts/backup.sh similarity index 100% rename from codebase/backend/deployment/scripts/backup.sh rename to VM1_app/codebase/backend/deployment/scripts/backup.sh diff --git a/codebase/backend/deployment/scripts/migrate.sh b/VM1_app/codebase/backend/deployment/scripts/migrate.sh similarity index 100% rename from codebase/backend/deployment/scripts/migrate.sh rename to VM1_app/codebase/backend/deployment/scripts/migrate.sh diff --git a/codebase/backend/deployment/scripts/rollback.sh b/VM1_app/codebase/backend/deployment/scripts/rollback.sh similarity index 100% rename from codebase/backend/deployment/scripts/rollback.sh rename to VM1_app/codebase/backend/deployment/scripts/rollback.sh diff --git a/codebase/backend/deployment/scripts/seed-personal-notifications-test.sh b/VM1_app/codebase/backend/deployment/scripts/seed-personal-notifications-test.sh similarity index 100% rename from codebase/backend/deployment/scripts/seed-personal-notifications-test.sh rename to VM1_app/codebase/backend/deployment/scripts/seed-personal-notifications-test.sh diff --git a/codebase/backend/deployment/scripts/seed.sh b/VM1_app/codebase/backend/deployment/scripts/seed.sh similarity index 100% rename from codebase/backend/deployment/scripts/seed.sh rename to VM1_app/codebase/backend/deployment/scripts/seed.sh diff --git a/codebase/backend/deployment/scripts/setup-vm.sh b/VM1_app/codebase/backend/deployment/scripts/setup-vm.sh similarity index 100% rename from codebase/backend/deployment/scripts/setup-vm.sh rename to VM1_app/codebase/backend/deployment/scripts/setup-vm.sh diff --git a/codebase/backend/deployment/scripts/smoke.sh b/VM1_app/codebase/backend/deployment/scripts/smoke.sh similarity index 100% rename from codebase/backend/deployment/scripts/smoke.sh rename to VM1_app/codebase/backend/deployment/scripts/smoke.sh diff --git a/codebase/backend/deployment/scripts/ssl-renew.sh b/VM1_app/codebase/backend/deployment/scripts/ssl-renew.sh similarity index 100% rename from codebase/backend/deployment/scripts/ssl-renew.sh rename to VM1_app/codebase/backend/deployment/scripts/ssl-renew.sh diff --git a/codebase/backend/deployment/scripts/verify-observability.sh b/VM1_app/codebase/backend/deployment/scripts/verify-observability.sh similarity index 100% rename from codebase/backend/deployment/scripts/verify-observability.sh rename to VM1_app/codebase/backend/deployment/scripts/verify-observability.sh diff --git a/codebase/backend/deployment/secrets/SELECTEL_RUNBOOK.ru.md b/VM1_app/codebase/backend/deployment/secrets/SELECTEL_RUNBOOK.ru.md similarity index 100% rename from codebase/backend/deployment/secrets/SELECTEL_RUNBOOK.ru.md rename to VM1_app/codebase/backend/deployment/secrets/SELECTEL_RUNBOOK.ru.md diff --git a/codebase/backend/deployment/secrets/config.example.json b/VM1_app/codebase/backend/deployment/secrets/config.example.json similarity index 100% rename from codebase/backend/deployment/secrets/config.example.json rename to VM1_app/codebase/backend/deployment/secrets/config.example.json diff --git a/codebase/backend/deployment/secrets/docker-han-secrets.conf.example b/VM1_app/codebase/backend/deployment/secrets/docker-han-secrets.conf.example similarity index 100% rename from codebase/backend/deployment/secrets/docker-han-secrets.conf.example rename to VM1_app/codebase/backend/deployment/secrets/docker-han-secrets.conf.example diff --git a/codebase/backend/deployment/secrets/han-compose b/VM1_app/codebase/backend/deployment/secrets/han-compose similarity index 100% rename from codebase/backend/deployment/secrets/han-compose rename to VM1_app/codebase/backend/deployment/secrets/han-compose diff --git a/codebase/backend/deployment/secrets/han-secrets b/VM1_app/codebase/backend/deployment/secrets/han-secrets similarity index 100% rename from codebase/backend/deployment/secrets/han-secrets rename to VM1_app/codebase/backend/deployment/secrets/han-secrets diff --git a/codebase/backend/deployment/secrets/han-secrets@.service b/VM1_app/codebase/backend/deployment/secrets/han-secrets@.service similarity index 100% rename from codebase/backend/deployment/secrets/han-secrets@.service rename to VM1_app/codebase/backend/deployment/secrets/han-secrets@.service diff --git a/codebase/backend/deployment/secrets/secrets_loader.py b/VM1_app/codebase/backend/deployment/secrets/secrets_loader.py similarity index 100% rename from codebase/backend/deployment/secrets/secrets_loader.py rename to VM1_app/codebase/backend/deployment/secrets/secrets_loader.py diff --git a/codebase/backend/docker-compose.yml b/VM1_app/codebase/backend/docker-compose.yml similarity index 100% rename from codebase/backend/docker-compose.yml rename to VM1_app/codebase/backend/docker-compose.yml diff --git a/codebase/backend/frontend-test-site/.dockerignore b/VM1_app/codebase/backend/frontend-test-site/.dockerignore similarity index 100% rename from codebase/backend/frontend-test-site/.dockerignore rename to VM1_app/codebase/backend/frontend-test-site/.dockerignore diff --git a/codebase/backend/frontend-test-site/.env.example b/VM1_app/codebase/backend/frontend-test-site/.env.example similarity index 100% rename from codebase/backend/frontend-test-site/.env.example rename to VM1_app/codebase/backend/frontend-test-site/.env.example diff --git a/codebase/backend/frontend-test-site/.gitignore b/VM1_app/codebase/backend/frontend-test-site/.gitignore similarity index 100% rename from codebase/backend/frontend-test-site/.gitignore rename to VM1_app/codebase/backend/frontend-test-site/.gitignore diff --git a/codebase/backend/frontend-test-site/Dockerfile b/VM1_app/codebase/backend/frontend-test-site/Dockerfile similarity index 100% rename from codebase/backend/frontend-test-site/Dockerfile rename to VM1_app/codebase/backend/frontend-test-site/Dockerfile diff --git a/codebase/backend/frontend-test-site/README.md b/VM1_app/codebase/backend/frontend-test-site/README.md similarity index 100% rename from codebase/backend/frontend-test-site/README.md rename to VM1_app/codebase/backend/frontend-test-site/README.md diff --git a/codebase/backend/frontend-test-site/app.config.ts b/VM1_app/codebase/backend/frontend-test-site/app.config.ts similarity index 100% rename from codebase/backend/frontend-test-site/app.config.ts rename to VM1_app/codebase/backend/frontend-test-site/app.config.ts diff --git a/codebase/backend/frontend-test-site/app/+html.tsx b/VM1_app/codebase/backend/frontend-test-site/app/+html.tsx similarity index 100% rename from codebase/backend/frontend-test-site/app/+html.tsx rename to VM1_app/codebase/backend/frontend-test-site/app/+html.tsx diff --git a/codebase/backend/frontend-test-site/app/_layout.tsx b/VM1_app/codebase/backend/frontend-test-site/app/_layout.tsx similarity index 100% rename from codebase/backend/frontend-test-site/app/_layout.tsx rename to VM1_app/codebase/backend/frontend-test-site/app/_layout.tsx diff --git a/codebase/backend/frontend-test-site/app/auth/callback.tsx b/VM1_app/codebase/backend/frontend-test-site/app/auth/callback.tsx similarity index 100% rename from codebase/backend/frontend-test-site/app/auth/callback.tsx rename to VM1_app/codebase/backend/frontend-test-site/app/auth/callback.tsx diff --git a/codebase/backend/frontend-test-site/app/diagnostics.tsx b/VM1_app/codebase/backend/frontend-test-site/app/diagnostics.tsx similarity index 100% rename from codebase/backend/frontend-test-site/app/diagnostics.tsx rename to VM1_app/codebase/backend/frontend-test-site/app/diagnostics.tsx diff --git a/codebase/backend/frontend-test-site/app/dialogs/[dialogId].tsx b/VM1_app/codebase/backend/frontend-test-site/app/dialogs/[dialogId].tsx similarity index 100% rename from codebase/backend/frontend-test-site/app/dialogs/[dialogId].tsx rename to VM1_app/codebase/backend/frontend-test-site/app/dialogs/[dialogId].tsx diff --git a/codebase/backend/frontend-test-site/app/dialogs/index.tsx b/VM1_app/codebase/backend/frontend-test-site/app/dialogs/index.tsx similarity index 100% rename from codebase/backend/frontend-test-site/app/dialogs/index.tsx rename to VM1_app/codebase/backend/frontend-test-site/app/dialogs/index.tsx diff --git a/codebase/backend/frontend-test-site/app/index.tsx b/VM1_app/codebase/backend/frontend-test-site/app/index.tsx similarity index 100% rename from codebase/backend/frontend-test-site/app/index.tsx rename to VM1_app/codebase/backend/frontend-test-site/app/index.tsx diff --git a/codebase/backend/frontend-test-site/app/notification/[id].tsx b/VM1_app/codebase/backend/frontend-test-site/app/notification/[id].tsx similarity index 100% rename from codebase/backend/frontend-test-site/app/notification/[id].tsx rename to VM1_app/codebase/backend/frontend-test-site/app/notification/[id].tsx diff --git a/codebase/backend/frontend-test-site/app/notifications/index.tsx b/VM1_app/codebase/backend/frontend-test-site/app/notifications/index.tsx similarity index 100% rename from codebase/backend/frontend-test-site/app/notifications/index.tsx rename to VM1_app/codebase/backend/frontend-test-site/app/notifications/index.tsx diff --git a/codebase/backend/frontend-test-site/app/profile.tsx b/VM1_app/codebase/backend/frontend-test-site/app/profile.tsx similarity index 100% rename from codebase/backend/frontend-test-site/app/profile.tsx rename to VM1_app/codebase/backend/frontend-test-site/app/profile.tsx diff --git a/codebase/backend/frontend-test-site/expo-env.d.ts b/VM1_app/codebase/backend/frontend-test-site/expo-env.d.ts similarity index 100% rename from codebase/backend/frontend-test-site/expo-env.d.ts rename to VM1_app/codebase/backend/frontend-test-site/expo-env.d.ts diff --git a/codebase/backend/frontend-test-site/package.json b/VM1_app/codebase/backend/frontend-test-site/package.json similarity index 100% rename from codebase/backend/frontend-test-site/package.json rename to VM1_app/codebase/backend/frontend-test-site/package.json diff --git a/codebase/backend/frontend-test-site/playwright.config.ts b/VM1_app/codebase/backend/frontend-test-site/playwright.config.ts similarity index 100% rename from codebase/backend/frontend-test-site/playwright.config.ts rename to VM1_app/codebase/backend/frontend-test-site/playwright.config.ts diff --git a/codebase/backend/frontend-test-site/public/apple-touch-icon.png b/VM1_app/codebase/backend/frontend-test-site/public/apple-touch-icon.png similarity index 100% rename from codebase/backend/frontend-test-site/public/apple-touch-icon.png rename to VM1_app/codebase/backend/frontend-test-site/public/apple-touch-icon.png diff --git a/codebase/backend/frontend-test-site/public/favicon.png b/VM1_app/codebase/backend/frontend-test-site/public/favicon.png similarity index 100% rename from codebase/backend/frontend-test-site/public/favicon.png rename to VM1_app/codebase/backend/frontend-test-site/public/favicon.png diff --git a/codebase/backend/frontend-test-site/public/icon-192.png b/VM1_app/codebase/backend/frontend-test-site/public/icon-192.png similarity index 100% rename from codebase/backend/frontend-test-site/public/icon-192.png rename to VM1_app/codebase/backend/frontend-test-site/public/icon-192.png diff --git a/codebase/backend/frontend-test-site/public/icon-512.png b/VM1_app/codebase/backend/frontend-test-site/public/icon-512.png similarity index 100% rename from codebase/backend/frontend-test-site/public/icon-512.png rename to VM1_app/codebase/backend/frontend-test-site/public/icon-512.png diff --git a/codebase/backend/frontend-test-site/public/manifest.json b/VM1_app/codebase/backend/frontend-test-site/public/manifest.json similarity index 100% rename from codebase/backend/frontend-test-site/public/manifest.json rename to VM1_app/codebase/backend/frontend-test-site/public/manifest.json diff --git a/codebase/backend/frontend-test-site/public/register-sw.js b/VM1_app/codebase/backend/frontend-test-site/public/register-sw.js similarity index 100% rename from codebase/backend/frontend-test-site/public/register-sw.js rename to VM1_app/codebase/backend/frontend-test-site/public/register-sw.js diff --git a/codebase/backend/frontend-test-site/scripts/generate-pwa-icons.ps1 b/VM1_app/codebase/backend/frontend-test-site/scripts/generate-pwa-icons.ps1 similarity index 100% rename from codebase/backend/frontend-test-site/scripts/generate-pwa-icons.ps1 rename to VM1_app/codebase/backend/frontend-test-site/scripts/generate-pwa-icons.ps1 diff --git a/codebase/backend/frontend-test-site/src/api.ts b/VM1_app/codebase/backend/frontend-test-site/src/api.ts similarity index 100% rename from codebase/backend/frontend-test-site/src/api.ts rename to VM1_app/codebase/backend/frontend-test-site/src/api.ts diff --git a/codebase/backend/frontend-test-site/src/app-context.tsx b/VM1_app/codebase/backend/frontend-test-site/src/app-context.tsx similarity index 100% rename from codebase/backend/frontend-test-site/src/app-context.tsx rename to VM1_app/codebase/backend/frontend-test-site/src/app-context.tsx diff --git a/codebase/backend/frontend-test-site/src/auth.ts b/VM1_app/codebase/backend/frontend-test-site/src/auth.ts similarity index 100% rename from codebase/backend/frontend-test-site/src/auth.ts rename to VM1_app/codebase/backend/frontend-test-site/src/auth.ts diff --git a/codebase/backend/frontend-test-site/src/components/AccordionSection.tsx b/VM1_app/codebase/backend/frontend-test-site/src/components/AccordionSection.tsx similarity index 100% rename from codebase/backend/frontend-test-site/src/components/AccordionSection.tsx rename to VM1_app/codebase/backend/frontend-test-site/src/components/AccordionSection.tsx diff --git a/codebase/backend/frontend-test-site/src/components/AppHeader.tsx b/VM1_app/codebase/backend/frontend-test-site/src/components/AppHeader.tsx similarity index 100% rename from codebase/backend/frontend-test-site/src/components/AppHeader.tsx rename to VM1_app/codebase/backend/frontend-test-site/src/components/AppHeader.tsx diff --git a/codebase/backend/frontend-test-site/src/components/AuthLoadingView.tsx b/VM1_app/codebase/backend/frontend-test-site/src/components/AuthLoadingView.tsx similarity index 100% rename from codebase/backend/frontend-test-site/src/components/AuthLoadingView.tsx rename to VM1_app/codebase/backend/frontend-test-site/src/components/AuthLoadingView.tsx diff --git a/codebase/backend/frontend-test-site/src/components/ChatInputBar.tsx b/VM1_app/codebase/backend/frontend-test-site/src/components/ChatInputBar.tsx similarity index 100% rename from codebase/backend/frontend-test-site/src/components/ChatInputBar.tsx rename to VM1_app/codebase/backend/frontend-test-site/src/components/ChatInputBar.tsx diff --git a/codebase/backend/frontend-test-site/src/components/ChatScreenHeader.tsx b/VM1_app/codebase/backend/frontend-test-site/src/components/ChatScreenHeader.tsx similarity index 100% rename from codebase/backend/frontend-test-site/src/components/ChatScreenHeader.tsx rename to VM1_app/codebase/backend/frontend-test-site/src/components/ChatScreenHeader.tsx diff --git a/codebase/backend/frontend-test-site/src/components/ConsentModal.tsx b/VM1_app/codebase/backend/frontend-test-site/src/components/ConsentModal.tsx similarity index 100% rename from codebase/backend/frontend-test-site/src/components/ConsentModal.tsx rename to VM1_app/codebase/backend/frontend-test-site/src/components/ConsentModal.tsx diff --git a/codebase/backend/frontend-test-site/src/components/GuestAuthGate.tsx b/VM1_app/codebase/backend/frontend-test-site/src/components/GuestAuthGate.tsx similarity index 100% rename from codebase/backend/frontend-test-site/src/components/GuestAuthGate.tsx rename to VM1_app/codebase/backend/frontend-test-site/src/components/GuestAuthGate.tsx diff --git a/codebase/backend/frontend-test-site/src/components/HanLogo.tsx b/VM1_app/codebase/backend/frontend-test-site/src/components/HanLogo.tsx similarity index 100% rename from codebase/backend/frontend-test-site/src/components/HanLogo.tsx rename to VM1_app/codebase/backend/frontend-test-site/src/components/HanLogo.tsx diff --git a/codebase/backend/frontend-test-site/src/components/MessageBubble.tsx b/VM1_app/codebase/backend/frontend-test-site/src/components/MessageBubble.tsx similarity index 100% rename from codebase/backend/frontend-test-site/src/components/MessageBubble.tsx rename to VM1_app/codebase/backend/frontend-test-site/src/components/MessageBubble.tsx diff --git a/codebase/backend/frontend-test-site/src/components/NotificationCard.tsx b/VM1_app/codebase/backend/frontend-test-site/src/components/NotificationCard.tsx similarity index 100% rename from codebase/backend/frontend-test-site/src/components/NotificationCard.tsx rename to VM1_app/codebase/backend/frontend-test-site/src/components/NotificationCard.tsx diff --git a/codebase/backend/frontend-test-site/src/components/NotificationCarousel.tsx b/VM1_app/codebase/backend/frontend-test-site/src/components/NotificationCarousel.tsx similarity index 100% rename from codebase/backend/frontend-test-site/src/components/NotificationCarousel.tsx rename to VM1_app/codebase/backend/frontend-test-site/src/components/NotificationCarousel.tsx diff --git a/codebase/backend/frontend-test-site/src/components/PopularQuestionsList.tsx b/VM1_app/codebase/backend/frontend-test-site/src/components/PopularQuestionsList.tsx similarity index 100% rename from codebase/backend/frontend-test-site/src/components/PopularQuestionsList.tsx rename to VM1_app/codebase/backend/frontend-test-site/src/components/PopularQuestionsList.tsx diff --git a/codebase/backend/frontend-test-site/src/components/QuickActions.tsx b/VM1_app/codebase/backend/frontend-test-site/src/components/QuickActions.tsx similarity index 100% rename from codebase/backend/frontend-test-site/src/components/QuickActions.tsx rename to VM1_app/codebase/backend/frontend-test-site/src/components/QuickActions.tsx diff --git a/codebase/backend/frontend-test-site/src/components/ScreenShell.tsx b/VM1_app/codebase/backend/frontend-test-site/src/components/ScreenShell.tsx similarity index 100% rename from codebase/backend/frontend-test-site/src/components/ScreenShell.tsx rename to VM1_app/codebase/backend/frontend-test-site/src/components/ScreenShell.tsx diff --git a/codebase/backend/frontend-test-site/src/config.ts b/VM1_app/codebase/backend/frontend-test-site/src/config.ts similarity index 100% rename from codebase/backend/frontend-test-site/src/config.ts rename to VM1_app/codebase/backend/frontend-test-site/src/config.ts diff --git a/codebase/backend/frontend-test-site/src/message-text.ts b/VM1_app/codebase/backend/frontend-test-site/src/message-text.ts similarity index 100% rename from codebase/backend/frontend-test-site/src/message-text.ts rename to VM1_app/codebase/backend/frontend-test-site/src/message-text.ts diff --git a/codebase/backend/frontend-test-site/src/notification-actions.ts b/VM1_app/codebase/backend/frontend-test-site/src/notification-actions.ts similarity index 100% rename from codebase/backend/frontend-test-site/src/notification-actions.ts rename to VM1_app/codebase/backend/frontend-test-site/src/notification-actions.ts diff --git a/codebase/backend/frontend-test-site/src/notification-api.ts b/VM1_app/codebase/backend/frontend-test-site/src/notification-api.ts similarity index 100% rename from codebase/backend/frontend-test-site/src/notification-api.ts rename to VM1_app/codebase/backend/frontend-test-site/src/notification-api.ts diff --git a/codebase/backend/frontend-test-site/src/notification-presenter.ts b/VM1_app/codebase/backend/frontend-test-site/src/notification-presenter.ts similarity index 100% rename from codebase/backend/frontend-test-site/src/notification-presenter.ts rename to VM1_app/codebase/backend/frontend-test-site/src/notification-presenter.ts diff --git a/codebase/backend/frontend-test-site/src/oidc-device.ts b/VM1_app/codebase/backend/frontend-test-site/src/oidc-device.ts similarity index 100% rename from codebase/backend/frontend-test-site/src/oidc-device.ts rename to VM1_app/codebase/backend/frontend-test-site/src/oidc-device.ts diff --git a/codebase/backend/frontend-test-site/src/pending-intent.ts b/VM1_app/codebase/backend/frontend-test-site/src/pending-intent.ts similarity index 100% rename from codebase/backend/frontend-test-site/src/pending-intent.ts rename to VM1_app/codebase/backend/frontend-test-site/src/pending-intent.ts diff --git a/codebase/backend/frontend-test-site/src/realtime.ts b/VM1_app/codebase/backend/frontend-test-site/src/realtime.ts similarity index 100% rename from codebase/backend/frontend-test-site/src/realtime.ts rename to VM1_app/codebase/backend/frontend-test-site/src/realtime.ts diff --git a/codebase/backend/frontend-test-site/src/reconcile.ts b/VM1_app/codebase/backend/frontend-test-site/src/reconcile.ts similarity index 100% rename from codebase/backend/frontend-test-site/src/reconcile.ts rename to VM1_app/codebase/backend/frontend-test-site/src/reconcile.ts diff --git a/codebase/backend/frontend-test-site/src/services.ts b/VM1_app/codebase/backend/frontend-test-site/src/services.ts similarity index 100% rename from codebase/backend/frontend-test-site/src/services.ts rename to VM1_app/codebase/backend/frontend-test-site/src/services.ts diff --git a/codebase/backend/frontend-test-site/src/session.ts b/VM1_app/codebase/backend/frontend-test-site/src/session.ts similarity index 100% rename from codebase/backend/frontend-test-site/src/session.ts rename to VM1_app/codebase/backend/frontend-test-site/src/session.ts diff --git a/codebase/backend/frontend-test-site/src/single-flight.ts b/VM1_app/codebase/backend/frontend-test-site/src/single-flight.ts similarity index 100% rename from codebase/backend/frontend-test-site/src/single-flight.ts rename to VM1_app/codebase/backend/frontend-test-site/src/single-flight.ts diff --git a/codebase/backend/frontend-test-site/src/theme.ts b/VM1_app/codebase/backend/frontend-test-site/src/theme.ts similarity index 100% rename from codebase/backend/frontend-test-site/src/theme.ts rename to VM1_app/codebase/backend/frontend-test-site/src/theme.ts diff --git a/codebase/backend/frontend-test-site/src/types.ts b/VM1_app/codebase/backend/frontend-test-site/src/types.ts similarity index 100% rename from codebase/backend/frontend-test-site/src/types.ts rename to VM1_app/codebase/backend/frontend-test-site/src/types.ts diff --git a/codebase/backend/frontend-test-site/src/ui.tsx b/VM1_app/codebase/backend/frontend-test-site/src/ui.tsx similarity index 100% rename from codebase/backend/frontend-test-site/src/ui.tsx rename to VM1_app/codebase/backend/frontend-test-site/src/ui.tsx diff --git a/codebase/backend/frontend-test-site/tests/e2e/smoke.spec.ts b/VM1_app/codebase/backend/frontend-test-site/tests/e2e/smoke.spec.ts similarity index 100% rename from codebase/backend/frontend-test-site/tests/e2e/smoke.spec.ts rename to VM1_app/codebase/backend/frontend-test-site/tests/e2e/smoke.spec.ts diff --git a/codebase/backend/frontend-test-site/tests/unit/core.test.ts b/VM1_app/codebase/backend/frontend-test-site/tests/unit/core.test.ts similarity index 100% rename from codebase/backend/frontend-test-site/tests/unit/core.test.ts rename to VM1_app/codebase/backend/frontend-test-site/tests/unit/core.test.ts diff --git a/codebase/backend/frontend-test-site/tests/unit/notifications.test.ts b/VM1_app/codebase/backend/frontend-test-site/tests/unit/notifications.test.ts similarity index 100% rename from codebase/backend/frontend-test-site/tests/unit/notifications.test.ts rename to VM1_app/codebase/backend/frontend-test-site/tests/unit/notifications.test.ts diff --git a/codebase/backend/frontend-test-site/tsconfig.json b/VM1_app/codebase/backend/frontend-test-site/tsconfig.json similarity index 100% rename from codebase/backend/frontend-test-site/tsconfig.json rename to VM1_app/codebase/backend/frontend-test-site/tsconfig.json diff --git a/codebase/backend/frontend-test-site/vitest.config.ts b/VM1_app/codebase/backend/frontend-test-site/vitest.config.ts similarity index 100% rename from codebase/backend/frontend-test-site/vitest.config.ts rename to VM1_app/codebase/backend/frontend-test-site/vitest.config.ts diff --git a/codebase/backend/frontend-test-site/workbox-config.js b/VM1_app/codebase/backend/frontend-test-site/workbox-config.js similarity index 100% rename from codebase/backend/frontend-test-site/workbox-config.js rename to VM1_app/codebase/backend/frontend-test-site/workbox-config.js diff --git a/codebase/backend/infra/compose/application.yml b/VM1_app/codebase/backend/infra/compose/application.yml similarity index 100% rename from codebase/backend/infra/compose/application.yml rename to VM1_app/codebase/backend/infra/compose/application.yml diff --git a/codebase/backend/keycloak/.dockerignore b/VM1_app/codebase/backend/keycloak/.dockerignore similarity index 100% rename from codebase/backend/keycloak/.dockerignore rename to VM1_app/codebase/backend/keycloak/.dockerignore diff --git a/codebase/backend/keycloak/.env.example b/VM1_app/codebase/backend/keycloak/.env.example similarity index 100% rename from codebase/backend/keycloak/.env.example rename to VM1_app/codebase/backend/keycloak/.env.example diff --git a/codebase/backend/keycloak/Dockerfile b/VM1_app/codebase/backend/keycloak/Dockerfile similarity index 100% rename from codebase/backend/keycloak/Dockerfile rename to VM1_app/codebase/backend/keycloak/Dockerfile diff --git a/codebase/backend/keycloak/README.md b/VM1_app/codebase/backend/keycloak/README.md similarity index 100% rename from codebase/backend/keycloak/README.md rename to VM1_app/codebase/backend/keycloak/README.md diff --git a/codebase/backend/keycloak/container-entrypoint.sh b/VM1_app/codebase/backend/keycloak/container-entrypoint.sh similarity index 100% rename from codebase/backend/keycloak/container-entrypoint.sh rename to VM1_app/codebase/backend/keycloak/container-entrypoint.sh diff --git a/codebase/backend/keycloak/docker-compose.yml b/VM1_app/codebase/backend/keycloak/docker-compose.yml similarity index 100% rename from codebase/backend/keycloak/docker-compose.yml rename to VM1_app/codebase/backend/keycloak/docker-compose.yml diff --git a/codebase/backend/keycloak/pom.xml b/VM1_app/codebase/backend/keycloak/pom.xml similarity index 100% rename from codebase/backend/keycloak/pom.xml rename to VM1_app/codebase/backend/keycloak/pom.xml diff --git a/codebase/backend/keycloak/realm/han-chat-realm.json b/VM1_app/codebase/backend/keycloak/realm/han-chat-realm.json similarity index 100% rename from codebase/backend/keycloak/realm/han-chat-realm.json rename to VM1_app/codebase/backend/keycloak/realm/han-chat-realm.json diff --git a/codebase/backend/keycloak/src/main/java/ru/han/chat/keycloak/Config.java b/VM1_app/codebase/backend/keycloak/src/main/java/ru/han/chat/keycloak/Config.java similarity index 100% rename from codebase/backend/keycloak/src/main/java/ru/han/chat/keycloak/Config.java rename to VM1_app/codebase/backend/keycloak/src/main/java/ru/han/chat/keycloak/Config.java diff --git a/codebase/backend/keycloak/src/main/java/ru/han/chat/keycloak/Crypto.java b/VM1_app/codebase/backend/keycloak/src/main/java/ru/han/chat/keycloak/Crypto.java similarity index 100% rename from codebase/backend/keycloak/src/main/java/ru/han/chat/keycloak/Crypto.java rename to VM1_app/codebase/backend/keycloak/src/main/java/ru/han/chat/keycloak/Crypto.java diff --git a/codebase/backend/keycloak/src/main/java/ru/han/chat/keycloak/DeviceMetadata.java b/VM1_app/codebase/backend/keycloak/src/main/java/ru/han/chat/keycloak/DeviceMetadata.java similarity index 100% rename from codebase/backend/keycloak/src/main/java/ru/han/chat/keycloak/DeviceMetadata.java rename to VM1_app/codebase/backend/keycloak/src/main/java/ru/han/chat/keycloak/DeviceMetadata.java diff --git a/codebase/backend/keycloak/src/main/java/ru/han/chat/keycloak/OtpFlow.java b/VM1_app/codebase/backend/keycloak/src/main/java/ru/han/chat/keycloak/OtpFlow.java similarity index 100% rename from codebase/backend/keycloak/src/main/java/ru/han/chat/keycloak/OtpFlow.java rename to VM1_app/codebase/backend/keycloak/src/main/java/ru/han/chat/keycloak/OtpFlow.java diff --git a/codebase/backend/keycloak/src/main/java/ru/han/chat/keycloak/OtpStore.java b/VM1_app/codebase/backend/keycloak/src/main/java/ru/han/chat/keycloak/OtpStore.java similarity index 100% rename from codebase/backend/keycloak/src/main/java/ru/han/chat/keycloak/OtpStore.java rename to VM1_app/codebase/backend/keycloak/src/main/java/ru/han/chat/keycloak/OtpStore.java diff --git a/codebase/backend/keycloak/src/main/java/ru/han/chat/keycloak/PhoneIdentityAuthenticator.java b/VM1_app/codebase/backend/keycloak/src/main/java/ru/han/chat/keycloak/PhoneIdentityAuthenticator.java similarity index 100% rename from codebase/backend/keycloak/src/main/java/ru/han/chat/keycloak/PhoneIdentityAuthenticator.java rename to VM1_app/codebase/backend/keycloak/src/main/java/ru/han/chat/keycloak/PhoneIdentityAuthenticator.java diff --git a/codebase/backend/keycloak/src/main/java/ru/han/chat/keycloak/PhoneIdentityAuthenticatorFactory.java b/VM1_app/codebase/backend/keycloak/src/main/java/ru/han/chat/keycloak/PhoneIdentityAuthenticatorFactory.java similarity index 100% rename from codebase/backend/keycloak/src/main/java/ru/han/chat/keycloak/PhoneIdentityAuthenticatorFactory.java rename to VM1_app/codebase/backend/keycloak/src/main/java/ru/han/chat/keycloak/PhoneIdentityAuthenticatorFactory.java diff --git a/codebase/backend/keycloak/src/main/java/ru/han/chat/keycloak/PhoneNormalizer.java b/VM1_app/codebase/backend/keycloak/src/main/java/ru/han/chat/keycloak/PhoneNormalizer.java similarity index 100% rename from codebase/backend/keycloak/src/main/java/ru/han/chat/keycloak/PhoneNormalizer.java rename to VM1_app/codebase/backend/keycloak/src/main/java/ru/han/chat/keycloak/PhoneNormalizer.java diff --git a/codebase/backend/keycloak/src/main/java/ru/han/chat/keycloak/PhoneOtpAuthenticator.java b/VM1_app/codebase/backend/keycloak/src/main/java/ru/han/chat/keycloak/PhoneOtpAuthenticator.java similarity index 100% rename from codebase/backend/keycloak/src/main/java/ru/han/chat/keycloak/PhoneOtpAuthenticator.java rename to VM1_app/codebase/backend/keycloak/src/main/java/ru/han/chat/keycloak/PhoneOtpAuthenticator.java diff --git a/codebase/backend/keycloak/src/main/java/ru/han/chat/keycloak/PhoneOtpAuthenticatorFactory.java b/VM1_app/codebase/backend/keycloak/src/main/java/ru/han/chat/keycloak/PhoneOtpAuthenticatorFactory.java similarity index 100% rename from codebase/backend/keycloak/src/main/java/ru/han/chat/keycloak/PhoneOtpAuthenticatorFactory.java rename to VM1_app/codebase/backend/keycloak/src/main/java/ru/han/chat/keycloak/PhoneOtpAuthenticatorFactory.java diff --git a/codebase/backend/keycloak/src/main/java/ru/han/chat/keycloak/SettingsBridge.java b/VM1_app/codebase/backend/keycloak/src/main/java/ru/han/chat/keycloak/SettingsBridge.java similarity index 100% rename from codebase/backend/keycloak/src/main/java/ru/han/chat/keycloak/SettingsBridge.java rename to VM1_app/codebase/backend/keycloak/src/main/java/ru/han/chat/keycloak/SettingsBridge.java diff --git a/codebase/backend/keycloak/src/main/java/ru/han/chat/keycloak/SmsOrderClient.java b/VM1_app/codebase/backend/keycloak/src/main/java/ru/han/chat/keycloak/SmsOrderClient.java similarity index 100% rename from codebase/backend/keycloak/src/main/java/ru/han/chat/keycloak/SmsOrderClient.java rename to VM1_app/codebase/backend/keycloak/src/main/java/ru/han/chat/keycloak/SmsOrderClient.java diff --git a/codebase/backend/keycloak/src/main/java/ru/han/chat/keycloak/YandexSmartCaptchaClient.java b/VM1_app/codebase/backend/keycloak/src/main/java/ru/han/chat/keycloak/YandexSmartCaptchaClient.java similarity index 100% rename from codebase/backend/keycloak/src/main/java/ru/han/chat/keycloak/YandexSmartCaptchaClient.java rename to VM1_app/codebase/backend/keycloak/src/main/java/ru/han/chat/keycloak/YandexSmartCaptchaClient.java diff --git a/codebase/backend/keycloak/src/main/java/ru/han/chat/keycloak/entity/OtpChallengeEntity.java b/VM1_app/codebase/backend/keycloak/src/main/java/ru/han/chat/keycloak/entity/OtpChallengeEntity.java similarity index 100% rename from codebase/backend/keycloak/src/main/java/ru/han/chat/keycloak/entity/OtpChallengeEntity.java rename to VM1_app/codebase/backend/keycloak/src/main/java/ru/han/chat/keycloak/entity/OtpChallengeEntity.java diff --git a/codebase/backend/keycloak/src/main/java/ru/han/chat/keycloak/entity/OtpSecurityEventEntity.java b/VM1_app/codebase/backend/keycloak/src/main/java/ru/han/chat/keycloak/entity/OtpSecurityEventEntity.java similarity index 100% rename from codebase/backend/keycloak/src/main/java/ru/han/chat/keycloak/entity/OtpSecurityEventEntity.java rename to VM1_app/codebase/backend/keycloak/src/main/java/ru/han/chat/keycloak/entity/OtpSecurityEventEntity.java diff --git a/codebase/backend/keycloak/src/main/java/ru/han/chat/keycloak/entity/OtpSendCounterEntity.java b/VM1_app/codebase/backend/keycloak/src/main/java/ru/han/chat/keycloak/entity/OtpSendCounterEntity.java similarity index 100% rename from codebase/backend/keycloak/src/main/java/ru/han/chat/keycloak/entity/OtpSendCounterEntity.java rename to VM1_app/codebase/backend/keycloak/src/main/java/ru/han/chat/keycloak/entity/OtpSendCounterEntity.java diff --git a/codebase/backend/keycloak/src/main/java/ru/han/chat/keycloak/persistence/HanJpaEntityProvider.java b/VM1_app/codebase/backend/keycloak/src/main/java/ru/han/chat/keycloak/persistence/HanJpaEntityProvider.java similarity index 100% rename from codebase/backend/keycloak/src/main/java/ru/han/chat/keycloak/persistence/HanJpaEntityProvider.java rename to VM1_app/codebase/backend/keycloak/src/main/java/ru/han/chat/keycloak/persistence/HanJpaEntityProvider.java diff --git a/codebase/backend/keycloak/src/main/java/ru/han/chat/keycloak/persistence/HanJpaEntityProviderFactory.java b/VM1_app/codebase/backend/keycloak/src/main/java/ru/han/chat/keycloak/persistence/HanJpaEntityProviderFactory.java similarity index 100% rename from codebase/backend/keycloak/src/main/java/ru/han/chat/keycloak/persistence/HanJpaEntityProviderFactory.java rename to VM1_app/codebase/backend/keycloak/src/main/java/ru/han/chat/keycloak/persistence/HanJpaEntityProviderFactory.java diff --git a/codebase/backend/keycloak/src/main/resources/META-INF/han-otp-changelog.xml b/VM1_app/codebase/backend/keycloak/src/main/resources/META-INF/han-otp-changelog.xml similarity index 100% rename from codebase/backend/keycloak/src/main/resources/META-INF/han-otp-changelog.xml rename to VM1_app/codebase/backend/keycloak/src/main/resources/META-INF/han-otp-changelog.xml diff --git a/codebase/backend/keycloak/src/main/resources/META-INF/services/org.keycloak.authentication.AuthenticatorFactory b/VM1_app/codebase/backend/keycloak/src/main/resources/META-INF/services/org.keycloak.authentication.AuthenticatorFactory similarity index 100% rename from codebase/backend/keycloak/src/main/resources/META-INF/services/org.keycloak.authentication.AuthenticatorFactory rename to VM1_app/codebase/backend/keycloak/src/main/resources/META-INF/services/org.keycloak.authentication.AuthenticatorFactory diff --git a/codebase/backend/keycloak/src/main/resources/META-INF/services/org.keycloak.connections.jpa.entityprovider.JpaEntityProviderFactory b/VM1_app/codebase/backend/keycloak/src/main/resources/META-INF/services/org.keycloak.connections.jpa.entityprovider.JpaEntityProviderFactory similarity index 100% rename from codebase/backend/keycloak/src/main/resources/META-INF/services/org.keycloak.connections.jpa.entityprovider.JpaEntityProviderFactory rename to VM1_app/codebase/backend/keycloak/src/main/resources/META-INF/services/org.keycloak.connections.jpa.entityprovider.JpaEntityProviderFactory diff --git a/codebase/backend/keycloak/src/test/java/ru/han/chat/keycloak/CryptoTest.java b/VM1_app/codebase/backend/keycloak/src/test/java/ru/han/chat/keycloak/CryptoTest.java similarity index 100% rename from codebase/backend/keycloak/src/test/java/ru/han/chat/keycloak/CryptoTest.java rename to VM1_app/codebase/backend/keycloak/src/test/java/ru/han/chat/keycloak/CryptoTest.java diff --git a/codebase/backend/keycloak/src/test/java/ru/han/chat/keycloak/PhoneNormalizerTest.java b/VM1_app/codebase/backend/keycloak/src/test/java/ru/han/chat/keycloak/PhoneNormalizerTest.java similarity index 100% rename from codebase/backend/keycloak/src/test/java/ru/han/chat/keycloak/PhoneNormalizerTest.java rename to VM1_app/codebase/backend/keycloak/src/test/java/ru/han/chat/keycloak/PhoneNormalizerTest.java diff --git a/codebase/backend/keycloak/src/test/java/ru/han/chat/keycloak/RealmContractTest.java b/VM1_app/codebase/backend/keycloak/src/test/java/ru/han/chat/keycloak/RealmContractTest.java similarity index 100% rename from codebase/backend/keycloak/src/test/java/ru/han/chat/keycloak/RealmContractTest.java rename to VM1_app/codebase/backend/keycloak/src/test/java/ru/han/chat/keycloak/RealmContractTest.java diff --git a/codebase/backend/keycloak/src/test/java/ru/han/chat/keycloak/SmsLifecycleContractTest.java b/VM1_app/codebase/backend/keycloak/src/test/java/ru/han/chat/keycloak/SmsLifecycleContractTest.java similarity index 100% rename from codebase/backend/keycloak/src/test/java/ru/han/chat/keycloak/SmsLifecycleContractTest.java rename to VM1_app/codebase/backend/keycloak/src/test/java/ru/han/chat/keycloak/SmsLifecycleContractTest.java diff --git a/codebase/backend/keycloak/src/test/java/ru/han/chat/keycloak/SmsOrderClientTest.java b/VM1_app/codebase/backend/keycloak/src/test/java/ru/han/chat/keycloak/SmsOrderClientTest.java similarity index 100% rename from codebase/backend/keycloak/src/test/java/ru/han/chat/keycloak/SmsOrderClientTest.java rename to VM1_app/codebase/backend/keycloak/src/test/java/ru/han/chat/keycloak/SmsOrderClientTest.java diff --git a/codebase/backend/keycloak/src/test/java/ru/han/chat/keycloak/YandexSmartCaptchaClientTest.java b/VM1_app/codebase/backend/keycloak/src/test/java/ru/han/chat/keycloak/YandexSmartCaptchaClientTest.java similarity index 100% rename from codebase/backend/keycloak/src/test/java/ru/han/chat/keycloak/YandexSmartCaptchaClientTest.java rename to VM1_app/codebase/backend/keycloak/src/test/java/ru/han/chat/keycloak/YandexSmartCaptchaClientTest.java diff --git a/codebase/backend/keycloak/themes/han-phone/login/messages/messages_ru.properties b/VM1_app/codebase/backend/keycloak/themes/han-phone/login/messages/messages_ru.properties similarity index 100% rename from codebase/backend/keycloak/themes/han-phone/login/messages/messages_ru.properties rename to VM1_app/codebase/backend/keycloak/themes/han-phone/login/messages/messages_ru.properties diff --git a/codebase/backend/keycloak/themes/han-phone/login/otp.ftl b/VM1_app/codebase/backend/keycloak/themes/han-phone/login/otp.ftl similarity index 100% rename from codebase/backend/keycloak/themes/han-phone/login/otp.ftl rename to VM1_app/codebase/backend/keycloak/themes/han-phone/login/otp.ftl diff --git a/codebase/backend/keycloak/themes/han-phone/login/phone.ftl b/VM1_app/codebase/backend/keycloak/themes/han-phone/login/phone.ftl similarity index 100% rename from codebase/backend/keycloak/themes/han-phone/login/phone.ftl rename to VM1_app/codebase/backend/keycloak/themes/han-phone/login/phone.ftl diff --git a/codebase/backend/keycloak/themes/han-phone/login/resources/css/han-login.css b/VM1_app/codebase/backend/keycloak/themes/han-phone/login/resources/css/han-login.css similarity index 100% rename from codebase/backend/keycloak/themes/han-phone/login/resources/css/han-login.css rename to VM1_app/codebase/backend/keycloak/themes/han-phone/login/resources/css/han-login.css diff --git a/codebase/backend/keycloak/themes/han-phone/login/resources/js/han-login.js b/VM1_app/codebase/backend/keycloak/themes/han-phone/login/resources/js/han-login.js similarity index 100% rename from codebase/backend/keycloak/themes/han-phone/login/resources/js/han-login.js rename to VM1_app/codebase/backend/keycloak/themes/han-phone/login/resources/js/han-login.js diff --git a/codebase/backend/keycloak/themes/han-phone/login/theme.properties b/VM1_app/codebase/backend/keycloak/themes/han-phone/login/theme.properties similarity index 100% rename from codebase/backend/keycloak/themes/han-phone/login/theme.properties rename to VM1_app/codebase/backend/keycloak/themes/han-phone/login/theme.properties diff --git a/codebase/backend/message-safety/Dockerfile b/VM1_app/codebase/backend/message-safety/Dockerfile similarity index 100% rename from codebase/backend/message-safety/Dockerfile rename to VM1_app/codebase/backend/message-safety/Dockerfile diff --git a/codebase/backend/message-safety/app/__init__.py b/VM1_app/codebase/backend/message-safety/app/__init__.py similarity index 100% rename from codebase/backend/message-safety/app/__init__.py rename to VM1_app/codebase/backend/message-safety/app/__init__.py diff --git a/codebase/backend/message-safety/app/main.py b/VM1_app/codebase/backend/message-safety/app/main.py similarity index 100% rename from codebase/backend/message-safety/app/main.py rename to VM1_app/codebase/backend/message-safety/app/main.py diff --git a/codebase/backend/message-safety/container-entrypoint.sh b/VM1_app/codebase/backend/message-safety/container-entrypoint.sh similarity index 100% rename from codebase/backend/message-safety/container-entrypoint.sh rename to VM1_app/codebase/backend/message-safety/container-entrypoint.sh diff --git a/codebase/backend/message-safety/openapi.yaml b/VM1_app/codebase/backend/message-safety/openapi.yaml similarity index 100% rename from codebase/backend/message-safety/openapi.yaml rename to VM1_app/codebase/backend/message-safety/openapi.yaml diff --git a/codebase/backend/message-safety/pyproject.toml b/VM1_app/codebase/backend/message-safety/pyproject.toml similarity index 100% rename from codebase/backend/message-safety/pyproject.toml rename to VM1_app/codebase/backend/message-safety/pyproject.toml diff --git a/codebase/backend/message-safety/tests/test_service.py b/VM1_app/codebase/backend/message-safety/tests/test_service.py similarity index 100% rename from codebase/backend/message-safety/tests/test_service.py rename to VM1_app/codebase/backend/message-safety/tests/test_service.py diff --git a/codebase/backend/nginx/Dockerfile b/VM1_app/codebase/backend/nginx/Dockerfile similarity index 100% rename from codebase/backend/nginx/Dockerfile rename to VM1_app/codebase/backend/nginx/Dockerfile diff --git a/codebase/backend/nginx/docker-compose.yml b/VM1_app/codebase/backend/nginx/docker-compose.yml similarity index 100% rename from codebase/backend/nginx/docker-compose.yml rename to VM1_app/codebase/backend/nginx/docker-compose.yml diff --git a/codebase/backend/nginx/nginx.conf.template b/VM1_app/codebase/backend/nginx/nginx.conf.template similarity index 100% rename from codebase/backend/nginx/nginx.conf.template rename to VM1_app/codebase/backend/nginx/nginx.conf.template diff --git a/codebase/backend/nginx/scripts/entrypoint.sh b/VM1_app/codebase/backend/nginx/scripts/entrypoint.sh similarity index 100% rename from codebase/backend/nginx/scripts/entrypoint.sh rename to VM1_app/codebase/backend/nginx/scripts/entrypoint.sh diff --git a/codebase/backend/nginx/snippets/proxy-common.conf b/VM1_app/codebase/backend/nginx/snippets/proxy-common.conf similarity index 100% rename from codebase/backend/nginx/snippets/proxy-common.conf rename to VM1_app/codebase/backend/nginx/snippets/proxy-common.conf diff --git a/codebase/backend/nginx/snippets/proxy-keycloak-captcha-csp.conf b/VM1_app/codebase/backend/nginx/snippets/proxy-keycloak-captcha-csp.conf similarity index 100% rename from codebase/backend/nginx/snippets/proxy-keycloak-captcha-csp.conf rename to VM1_app/codebase/backend/nginx/snippets/proxy-keycloak-captcha-csp.conf diff --git a/codebase/backend/nginx/snippets/proxy-keycloak.conf b/VM1_app/codebase/backend/nginx/snippets/proxy-keycloak.conf similarity index 100% rename from codebase/backend/nginx/snippets/proxy-keycloak.conf rename to VM1_app/codebase/backend/nginx/snippets/proxy-keycloak.conf diff --git a/codebase/backend/nginx/snippets/security-headers.conf b/VM1_app/codebase/backend/nginx/snippets/security-headers.conf similarity index 100% rename from codebase/backend/nginx/snippets/security-headers.conf rename to VM1_app/codebase/backend/nginx/snippets/security-headers.conf diff --git a/codebase/backend/nginx/templates/frontend-dev.conf.template b/VM1_app/codebase/backend/nginx/templates/frontend-dev.conf.template similarity index 100% rename from codebase/backend/nginx/templates/frontend-dev.conf.template rename to VM1_app/codebase/backend/nginx/templates/frontend-dev.conf.template diff --git a/codebase/backend/nginx/templates/frontend-static.conf.template b/VM1_app/codebase/backend/nginx/templates/frontend-static.conf.template similarity index 100% rename from codebase/backend/nginx/templates/frontend-static.conf.template rename to VM1_app/codebase/backend/nginx/templates/frontend-static.conf.template diff --git a/codebase/backend/nginx/templates/security-headers.conf.template b/VM1_app/codebase/backend/nginx/templates/security-headers.conf.template similarity index 100% rename from codebase/backend/nginx/templates/security-headers.conf.template rename to VM1_app/codebase/backend/nginx/templates/security-headers.conf.template diff --git a/codebase/backend/nginx/templates/site-bootstrap.conf.template b/VM1_app/codebase/backend/nginx/templates/site-bootstrap.conf.template similarity index 100% rename from codebase/backend/nginx/templates/site-bootstrap.conf.template rename to VM1_app/codebase/backend/nginx/templates/site-bootstrap.conf.template diff --git a/codebase/backend/nginx/templates/site-tls.conf.template b/VM1_app/codebase/backend/nginx/templates/site-tls.conf.template similarity index 100% rename from codebase/backend/nginx/templates/site-tls.conf.template rename to VM1_app/codebase/backend/nginx/templates/site-tls.conf.template diff --git a/codebase/backend/observability/docker-compose.yml b/VM1_app/codebase/backend/observability/docker-compose.yml similarity index 100% rename from codebase/backend/observability/docker-compose.yml rename to VM1_app/codebase/backend/observability/docker-compose.yml diff --git a/codebase/backend/observability/otel-collector.local.yaml b/VM1_app/codebase/backend/observability/otel-collector.local.yaml similarity index 100% rename from codebase/backend/observability/otel-collector.local.yaml rename to VM1_app/codebase/backend/observability/otel-collector.local.yaml diff --git a/codebase/backend/observability/otel-collector.yaml b/VM1_app/codebase/backend/observability/otel-collector.yaml similarity index 100% rename from codebase/backend/observability/otel-collector.yaml rename to VM1_app/codebase/backend/observability/otel-collector.yaml diff --git a/codebase/backend/redis/Dockerfile b/VM1_app/codebase/backend/redis/Dockerfile similarity index 100% rename from codebase/backend/redis/Dockerfile rename to VM1_app/codebase/backend/redis/Dockerfile diff --git a/codebase/backend/redis/docker-compose.yml b/VM1_app/codebase/backend/redis/docker-compose.yml similarity index 100% rename from codebase/backend/redis/docker-compose.yml rename to VM1_app/codebase/backend/redis/docker-compose.yml diff --git a/codebase/backend/redis/redis.conf b/VM1_app/codebase/backend/redis/redis.conf similarity index 100% rename from codebase/backend/redis/redis.conf rename to VM1_app/codebase/backend/redis/redis.conf diff --git a/codebase/backend/redis/scripts/entrypoint.sh b/VM1_app/codebase/backend/redis/scripts/entrypoint.sh similarity index 100% rename from codebase/backend/redis/scripts/entrypoint.sh rename to VM1_app/codebase/backend/redis/scripts/entrypoint.sh diff --git a/codebase/backend/redis/users.acl.template b/VM1_app/codebase/backend/redis/users.acl.template similarity index 100% rename from codebase/backend/redis/users.acl.template rename to VM1_app/codebase/backend/redis/users.acl.template diff --git a/codebase/backend/scripts/validate-env b/VM1_app/codebase/backend/scripts/validate-env similarity index 100% rename from codebase/backend/scripts/validate-env rename to VM1_app/codebase/backend/scripts/validate-env diff --git a/codebase/backend/sms-service/Dockerfile b/VM1_app/codebase/backend/sms-service/Dockerfile similarity index 100% rename from codebase/backend/sms-service/Dockerfile rename to VM1_app/codebase/backend/sms-service/Dockerfile diff --git a/codebase/backend/sms-service/alembic.ini b/VM1_app/codebase/backend/sms-service/alembic.ini similarity index 100% rename from codebase/backend/sms-service/alembic.ini rename to VM1_app/codebase/backend/sms-service/alembic.ini diff --git a/codebase/backend/sms-service/app/__init__.py b/VM1_app/codebase/backend/sms-service/app/__init__.py similarity index 100% rename from codebase/backend/sms-service/app/__init__.py rename to VM1_app/codebase/backend/sms-service/app/__init__.py diff --git a/codebase/backend/sms-service/app/db.py b/VM1_app/codebase/backend/sms-service/app/db.py similarity index 100% rename from codebase/backend/sms-service/app/db.py rename to VM1_app/codebase/backend/sms-service/app/db.py diff --git a/codebase/backend/sms-service/app/domain.py b/VM1_app/codebase/backend/sms-service/app/domain.py similarity index 100% rename from codebase/backend/sms-service/app/domain.py rename to VM1_app/codebase/backend/sms-service/app/domain.py diff --git a/codebase/backend/sms-service/app/logging_security.py b/VM1_app/codebase/backend/sms-service/app/logging_security.py similarity index 100% rename from codebase/backend/sms-service/app/logging_security.py rename to VM1_app/codebase/backend/sms-service/app/logging_security.py diff --git a/codebase/backend/sms-service/app/main.py b/VM1_app/codebase/backend/sms-service/app/main.py similarity index 100% rename from codebase/backend/sms-service/app/main.py rename to VM1_app/codebase/backend/sms-service/app/main.py diff --git a/codebase/backend/sms-service/app/metrics.py b/VM1_app/codebase/backend/sms-service/app/metrics.py similarity index 100% rename from codebase/backend/sms-service/app/metrics.py rename to VM1_app/codebase/backend/sms-service/app/metrics.py diff --git a/codebase/backend/sms-service/app/provider.py b/VM1_app/codebase/backend/sms-service/app/provider.py similarity index 100% rename from codebase/backend/sms-service/app/provider.py rename to VM1_app/codebase/backend/sms-service/app/provider.py diff --git a/codebase/backend/sms-service/app/schemas.py b/VM1_app/codebase/backend/sms-service/app/schemas.py similarity index 100% rename from codebase/backend/sms-service/app/schemas.py rename to VM1_app/codebase/backend/sms-service/app/schemas.py diff --git a/codebase/backend/sms-service/app/service.py b/VM1_app/codebase/backend/sms-service/app/service.py similarity index 100% rename from codebase/backend/sms-service/app/service.py rename to VM1_app/codebase/backend/sms-service/app/service.py diff --git a/codebase/backend/sms-service/app/settings.py b/VM1_app/codebase/backend/sms-service/app/settings.py similarity index 100% rename from codebase/backend/sms-service/app/settings.py rename to VM1_app/codebase/backend/sms-service/app/settings.py diff --git a/codebase/backend/sms-service/app/telemetry.py b/VM1_app/codebase/backend/sms-service/app/telemetry.py similarity index 100% rename from codebase/backend/sms-service/app/telemetry.py rename to VM1_app/codebase/backend/sms-service/app/telemetry.py diff --git a/codebase/backend/sms-service/app/worker.py b/VM1_app/codebase/backend/sms-service/app/worker.py similarity index 100% rename from codebase/backend/sms-service/app/worker.py rename to VM1_app/codebase/backend/sms-service/app/worker.py diff --git a/codebase/backend/sms-service/container-entrypoint.sh b/VM1_app/codebase/backend/sms-service/container-entrypoint.sh similarity index 100% rename from codebase/backend/sms-service/container-entrypoint.sh rename to VM1_app/codebase/backend/sms-service/container-entrypoint.sh diff --git a/codebase/backend/sms-service/migrations/env.py b/VM1_app/codebase/backend/sms-service/migrations/env.py similarity index 100% rename from codebase/backend/sms-service/migrations/env.py rename to VM1_app/codebase/backend/sms-service/migrations/env.py diff --git a/codebase/backend/sms-service/migrations/versions/0001_initial.py b/VM1_app/codebase/backend/sms-service/migrations/versions/0001_initial.py similarity index 100% rename from codebase/backend/sms-service/migrations/versions/0001_initial.py rename to VM1_app/codebase/backend/sms-service/migrations/versions/0001_initial.py diff --git a/codebase/backend/sms-service/migrations/versions/0002_seed.py b/VM1_app/codebase/backend/sms-service/migrations/versions/0002_seed.py similarity index 100% rename from codebase/backend/sms-service/migrations/versions/0002_seed.py rename to VM1_app/codebase/backend/sms-service/migrations/versions/0002_seed.py diff --git a/codebase/backend/sms-service/openapi.yaml b/VM1_app/codebase/backend/sms-service/openapi.yaml similarity index 100% rename from codebase/backend/sms-service/openapi.yaml rename to VM1_app/codebase/backend/sms-service/openapi.yaml diff --git a/codebase/backend/sms-service/pyproject.toml b/VM1_app/codebase/backend/sms-service/pyproject.toml similarity index 100% rename from codebase/backend/sms-service/pyproject.toml rename to VM1_app/codebase/backend/sms-service/pyproject.toml diff --git a/codebase/backend/sms-service/tests/contract/test_openapi.py b/VM1_app/codebase/backend/sms-service/tests/contract/test_openapi.py similarity index 100% rename from codebase/backend/sms-service/tests/contract/test_openapi.py rename to VM1_app/codebase/backend/sms-service/tests/contract/test_openapi.py diff --git a/codebase/backend/sms-service/tests/unit/test_auth.py b/VM1_app/codebase/backend/sms-service/tests/unit/test_auth.py similarity index 100% rename from codebase/backend/sms-service/tests/unit/test_auth.py rename to VM1_app/codebase/backend/sms-service/tests/unit/test_auth.py diff --git a/codebase/backend/sms-service/tests/unit/test_domain.py b/VM1_app/codebase/backend/sms-service/tests/unit/test_domain.py similarity index 100% rename from codebase/backend/sms-service/tests/unit/test_domain.py rename to VM1_app/codebase/backend/sms-service/tests/unit/test_domain.py diff --git a/codebase/backend/sms-service/tests/unit/test_logging_security.py b/VM1_app/codebase/backend/sms-service/tests/unit/test_logging_security.py similarity index 100% rename from codebase/backend/sms-service/tests/unit/test_logging_security.py rename to VM1_app/codebase/backend/sms-service/tests/unit/test_logging_security.py diff --git a/codebase/backend/sms-service/tests/unit/test_provider.py b/VM1_app/codebase/backend/sms-service/tests/unit/test_provider.py similarity index 100% rename from codebase/backend/sms-service/tests/unit/test_provider.py rename to VM1_app/codebase/backend/sms-service/tests/unit/test_provider.py diff --git a/codebase/backend/sms-service/tests/unit/test_schemas.py b/VM1_app/codebase/backend/sms-service/tests/unit/test_schemas.py similarity index 100% rename from codebase/backend/sms-service/tests/unit/test_schemas.py rename to VM1_app/codebase/backend/sms-service/tests/unit/test_schemas.py diff --git a/codebase/backend/sms-service/tests/unit/test_telemetry.py b/VM1_app/codebase/backend/sms-service/tests/unit/test_telemetry.py similarity index 100% rename from codebase/backend/sms-service/tests/unit/test_telemetry.py rename to VM1_app/codebase/backend/sms-service/tests/unit/test_telemetry.py diff --git a/codebase/backend/tests/test_config.py b/VM1_app/codebase/backend/tests/test_config.py similarity index 100% rename from codebase/backend/tests/test_config.py rename to VM1_app/codebase/backend/tests/test_config.py diff --git a/codebase/backend/tests/test_secret_hygiene.py b/VM1_app/codebase/backend/tests/test_secret_hygiene.py similarity index 100% rename from codebase/backend/tests/test_secret_hygiene.py rename to VM1_app/codebase/backend/tests/test_secret_hygiene.py diff --git a/codebase/backend/tests/test_secrets_loader.py b/VM1_app/codebase/backend/tests/test_secrets_loader.py similarity index 100% rename from codebase/backend/tests/test_secrets_loader.py rename to VM1_app/codebase/backend/tests/test_secrets_loader.py diff --git a/VM1_app/documentation/README.md b/VM1_app/documentation/README.md new file mode 100644 index 0000000..e66b732 --- /dev/null +++ b/VM1_app/documentation/README.md @@ -0,0 +1,17 @@ +# Документация ВМ1 HAN Chat + +Здесь находятся профильные спецификации контура ВМ1. Канонические границы, имена и межсервисные контракты задаёт [`architectory`](../../architectory/README.md); при конфликте действует порядок приоритетов из этого README. + +## Состав ВМ1 + +- [`module-01-api-backend.md`](module-01-api-backend.md) — API, App DB, Message Safety caller, realtime. +- [`module-02-frontend-test-site.md`](module-02-frontend-test-site.md) — web/mobile frontend contract. +- [`module-03-nginx-vm1.md`](module-03-nginx-vm1.md) — public edge ВМ1. +- [`module-04-redis-vm1.md`](module-04-redis-vm1.md) — Redis DB0/DB1. +- [`module-06-bitrix-local-app.md`](module-06-bitrix-local-app.md) — Bitrix24 Open Lines local app. +- [`module-08-keycloak.md`](module-08-keycloak.md) — OIDC/OTP/SmartCaptcha. +- [`module-09-observability-vm1.md`](module-09-observability-vm1.md) — telemetry ВМ1. +- [`module-10-deployment-vm1.md`](module-10-deployment-vm1.md) — runbook ВМ1. +- [`module-11-idgtl-sms.md`](module-11-idgtl-sms.md) — целевой SMS-контур. + +Message Safety, CRM sync, nginx/Redis/telemetry и runbook ВМ2 находятся в [`VM2_services/documentation`](../../VM2_services/documentation/README.md). После cutover ВМ1 не запускает `message-safety`, `bitrix-sync` или Redis DB2; вызов Safety идёт по private HTTPS `:8443`. diff --git a/modules/module-01-api-backend.md b/VM1_app/documentation/module-01-api-backend.md similarity index 93% rename from modules/module-01-api-backend.md rename to VM1_app/documentation/module-01-api-backend.md index 6c7242a..ebb7017 100644 --- a/modules/module-01-api-backend.md +++ b/VM1_app/documentation/module-01-api-backend.md @@ -2,7 +2,7 @@ > Статус: целевая спецификация реализации MVP. > Язык реализации: Python, FastAPI. -> Канонические источники: [`README.md`](README.md), [`arch-00-glossary.md`](arch-00-glossary.md), [`arch-01-system-architecture.md`](arch-01-system-architecture.md), [`arch-02-api-contracts.md`](arch-02-api-contracts.md), [`arch-03-docker-compose-blueprint.md`](arch-03-docker-compose-blueprint.md), [`arch-04-settings-and-content.md`](arch-04-settings-and-content.md), [`arch-05-agent-development-process.md`](arch-05-agent-development-process.md). +> Канонические источники: [`README.md`](README.md), [`arch-00-glossary.md`](../../architectory/arch-00-glossary.md), [`arch-01-system-architecture.md`](../../architectory/arch-01-system-architecture.md), [`arch-02-api-contracts.md`](../../architectory/arch-02-api-contracts.md), [`arch-03-docker-compose-blueprint.md`](../../architectory/arch-03-docker-compose-blueprint.md), [`arch-04-settings-and-content.md`](../../architectory/arch-04-settings-and-content.md), [`arch-05-agent-development-process.md`](../../architectory/arch-05-agent-development-process.md), [`arch-06-service-hosting-security.md`](../../architectory/arch-06-service-hosting-security.md). ## 1. Назначение и приоритет @@ -519,6 +519,10 @@ Private network + Bearer `KEYCLOAK_SETTINGS_BRIDGE_TOKEN`. { "max_send_attempts_per_24h":3, "min_seconds_between_attempts":30, + "max_verify_attempts":5, + "code_length":6, + "ttl_seconds":300, + "sms_order_timeout_ms":75000, "version":"2026-07-10T08:00:00Z", "cache_ttl_seconds":60 } @@ -572,7 +576,7 @@ JWKS cache имеет positive TTL и short negative TTL для неизвест ### 8.3. User resolution -После bootstrap `sub` разрешается в active `UserIdentity`. Protected endpoint, кроме bootstrap, при отсутствии локального пользователя возвращает `409 resource_state_conflict` с безопасным сообщением «bootstrap required». **TBD-1:** arch-02 допускает `404/409` только для consents; перед публикацией OpenAPI выбрать единый код. До решения используется `409`. +После bootstrap `sub` разрешается в active `UserIdentity`. Protected endpoint, кроме bootstrap, при отсутствии локального пользователя возвращает канонический `409 resource_state_conflict` с безопасным сообщением «bootstrap required» по arch-02. ## 9. PostgreSQL: схема `han_app` @@ -791,12 +795,12 @@ Isolation default READ COMMITTED. Для конкурентных state transiti | Key | Value | TTL | |---|---|---| -| `han:api:rl:user:{user_id}:{route}:{window}` | counter/token bucket | длина окна + jitter | -| `han:api:rl:ip:{ip_hash}:{route}:{window}` | counter | длина окна + jitter | +| `han:api:rl:user:{user_id}:{route_hash}:{window}` | ZSET timestamps либо counter | длина окна + jitter | +| `han:api:rl:ip:{ip_hmac}:{route_hash}:{window}` | ZSET/counter | длина окна + jitter | | `han:api:rl:dialog:{dialog_id}:message:{window}` | counter | длина окна + jitter | -| `han:api:rl:service:{service}:{route}:{window}` | counter | длина окна + jitter | -| `han:api:idem:{scope}:{user_id}:{key_hash}` | state, fingerprint, response | 24 часа | -| `han:api:idemlock:{scope}:{user_id}:{key_hash}` | owner token | 30 секунд, продлевается heartbeat | +| `han:api:rl:service:{service}:{route_hash}:{window}` | counter/token bucket | длина окна + jitter | +| `han:api:idem:{scope}:{user_id}:{key_hmac}` | state, fingerprint, response | 24 часа | +| `han:api:idemlock:{scope}:{user_id}:{key_hmac}` | owner token | 30 секунд, продлевается heartbeat | | `han:api:jwks:negative:{kid_hash}` | marker | 30 секунд | Rate limit выполняется atomic Lua script. При превышении возвращаются `429`, `Retry-After` и audit только для значимых/повторных abuse случаев. @@ -805,16 +809,19 @@ Rate limit выполняется atomic Lua script. При превышении | Key/channel | Назначение | TTL | |---|---|---| -| `han:rt:user:{user_id}:connections` | set connection ids | heartbeat 90 сек | -| `han:rt:conn:{connection_id}` | user, subscriptions, server instance | 90 сек | +| `han:rt:user:{user_id}:connections` | ZSET connection id → heartbeat | 120 сек | +| `han:rt:conn:{connection_id}` | HASH: user, subscriptions, server instance, last_seen | 90 сек | | `han:rt:dialog:{dialog_id}` | Pub/Sub channel | сообщения не сохраняются | | `han:rt:user:{user_id}` | Pub/Sub channel | сообщения не сохраняются | | `han:coord:lock:safety-recovery:{task_id}` | distributed lock | 30 сек | | `han:coord:lock:delivery:{message_id}` | distributed lock | 30 сек | +| `han:coord:lock:settings-refresh:{instance}` | distributed lock | 30 сек | | `han:settings:snapshot:{version}` | optional serialized public snapshot | 5 минут | Redis Pub/Sub — ускоритель, не durable event bus. После reconnect клиент обязательно выполняет REST polling. Потеря DB1 не теряет сообщения. +Канонический формат всех Redis keys, типов и TTL принадлежит [`module-04-redis-vm1.md`](module-04-redis-vm1.md); эта таблица не вводит альтернативный namespace. + ## 12. Idempotency Fingerprint = SHA-256 от canonical method + route template + normalized path params + canonical JSON body + authenticated user id. Authorization, request-id и UX-session не входят. @@ -875,9 +882,13 @@ else: return mapped dependency error ``` +Перед POST caller формирует strict wire DTO module-05 §8.1: `checksum_sha256` из App DB передаётся как `attachment.checksum` с prefix `sha256:`, `quarantine_version_id` — как `attachment.quarantine_version_id`, ETag — как `attachment.quarantine_etag`. Unknown fields не отправляются; text/file union проверяется до вызова. + Polling interval начинается с server `Retry-After`, допускает capped exponential backoff и jitter, но не превышает caller env `MESSAGE_SAFETY_TASK_POLL_MAX_SEC`. Каждый v2 verdict/pending содержит `processing_mode` и `config_version`; MOCK возвращает только sync `200/403`. Клиенту internal mode/config/`202` не возвращаются: public POST сохраняет синхронную семантику. Legacy stub `/v1` с `203`/`stub_final_error` поддерживается только временным adapter-ом до cutover и не является target production path. -Capability snapshot `/health/ready` допускается кэшировать не дольше 5 с для fast-fail: file требует `files`, text с URL — `links`, text без URL — `text`. При `processing_mode=mock` normal capabilities имеют состояние `bypassed` и не применяются как fast-fail gate. Snapshot не является correctness gate: definitive capability повторно проверяет `POST /check`. При unavailable в standard mode api-backend возвращает public `503 dependency_unavailable`, не создаёт delivery outbox и не меняет status на blocked. +Capability snapshot `GET /internal/safety/status` через private `:8443` допускается кэшировать не дольше 5 с для fast-fail: file требует `files`, text с URL — `links`, text без URL — `text`. При `processing_mode=mock` normal capabilities имеют состояние `bypassed` и не применяются как fast-fail gate. Snapshot не является correctness gate: definitive capability повторно проверяет `POST /check`. При unavailable в standard mode api-backend возвращает public `503 dependency_unavailable`, не создаёт delivery outbox и не меняет status на blocked. + +`Location` из `202` принимается только как origin-relative path `/internal/safety/v2/messages/tasks/{task_id}` и резолвится относительно origin `MESSAGE_SAFETY_URL`. Absolute URL, другой host или иной prefix отклоняются без HTTP-запроса; recovery применяет то же правило. ### 13.2. Final allow @@ -1062,7 +1073,7 @@ Runtime refresh: poll `MAX(updated_at)` каждые 30 секунд; новый - `DATABASE_URL`; - `REDIS_URL`, `REDIS_REALTIME_URL`; - `KEYCLOAK_PUBLIC_URL`, `KEYCLOAK_INTERNAL_URL`, `KEYCLOAK_REALM`, `KEYCLOAK_AUDIENCE`; -- `MESSAGE_SAFETY_URL`, `MESSAGE_SAFETY_SERVICE_TOKEN`; +- `MESSAGE_SAFETY_URL`, `MESSAGE_SAFETY_API_PREFIX`, `MESSAGE_SAFETY_CA_FILE`, `MESSAGE_SAFETY_SERVICE_TOKEN`; - `MESSAGE_SAFETY_POST_TIMEOUT_SEC`, `MESSAGE_SAFETY_TASK_POLL_INTERVAL_SEC`, `MESSAGE_SAFETY_TASK_POLL_MAX_SEC`; - `MESSAGE_SAFETY_CIRCUIT_FAILURE_THRESHOLD`, `MESSAGE_SAFETY_CIRCUIT_OPEN_SEC`; - `BITRIX_LOCAL_APP_BASE_URL`, `BITRIX_LOCAL_APP_INTERNAL_TOKEN`, `BITRIX_API_INBOX_TOKEN`; @@ -1074,6 +1085,8 @@ Runtime refresh: poll `MAX(updated_at)` каждые 30 секунд; новый `SELECTEL_S3_QUARANTINE_READ_*` принадлежит `message-safety`, не должен передаваться контейнеру API. Новые env сначала документируются в arch-04. +В production validator принимает только remote `https://: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`. + ## 19. Rate limiting Два слоя обязательны: nginx edge и API Redis. @@ -1189,10 +1202,10 @@ Audit записывается в той же транзакции с крити - наличие валидного JWKS cache/discovery; - обязательный settings snapshot; - S3 permissions для presign/Head/copy/delete через безопасную capability check без создания orphan; -- Message Safety readiness; +- Message Safety readiness как dependency status, но не core readiness gate; - worker heartbeat/backlog thresholds. -Open Lines недоступность отображается как component `degraded`, но не обязательно делает весь API not-ready, чтобы чтение продолжало работать. **Решение M7:** readiness HTTP `200` при доступных DB/auth/settings и `status=degraded` для Bitrix/S3 partial failure; `503` — когда сервис не способен безопасно обслуживать большинство protected API. Compose healthcheck оценивает HTTP code, мониторинг — component details. +Open Lines или remote Message Safety недоступность отображается как component `degraded`, но не делает весь API not-ready, чтобы auth и чтение продолжали работать. Send path при недоступном Safety остаётся fail-closed. **Решение M7:** readiness HTTP `200` при доступных DB/auth/settings и `status=degraded` для Safety/Bitrix/S3 partial failure; `503` — когда сервис не способен безопасно обслуживать большинство protected API. Compose healthcheck оценивает HTTP code, мониторинг — component details. Ответ не содержит secrets/internal credentials: @@ -1202,7 +1215,7 @@ Open Lines недоступность отображается как component ## 23. Security -- TLS только через nginx, internal HTTP только Docker backend network. +- Public TLS терминирует nginx ВМ1; internal HTTP допустим только внутри Docker backend network одной VM. МежVM вызов Message Safety выполняется напрямую из `api-backend` через private HTTPS nginx ВМ2 `:8443` с проверкой internal CA и без fallback на Docker DNS/plaintext. - Доверять proxy headers только от известных proxy CIDR. - JWT validation fail-closed; алгоритм pinning; JWKS SSRF невозможен — URL строится из configured issuer/discovery. - Service tokens сравниваются constant-time; rotation поддерживает current + previous token в короткое окно только после документирования env. @@ -1386,7 +1399,7 @@ Bitrix event → local app durable inbox → `POST /internal/openlines/v1/inbox` - **M2:** phone claim `phone_number`, fallback `preferred_username` только E.164. - **M3:** Message создаётся до safety для durable crash checkpoint. - **M4:** Redis idempotency дополнен durable PostgreSQL record. -- **M5:** recovery продолжается после client timeout; точный extended budget — TBD. +- **M5:** recovery продолжается после client timeout; initial extended budget `HAN_APP_SAFETY_RECOVERY_MAX_SEC=1200`. - **M6:** WS subprotocol предпочтительнее query token. - **M7:** readiness различает critical not-ready и partial degraded. - **M8:** denied text редактируется/не хранится в открытом виде. @@ -1399,8 +1412,6 @@ Bitrix event → local app durable inbox → `POST /internal/openlines/v1/inbox` ### Требуют согласования -- **TBD-1:** единый код для protected endpoint до bootstrap (`409` предложен). -- **Решение M5:** extended recovery ограничен `HAN_APP_SAFETY_RECOVERY_MAX_SEC=1200` и Safety `expires_at`. - **TBD-3:** добавить `event_id`/`occurred_at` в WS events и `event_id` в inbox OpenAPI. - **TBD-4:** production SLO, RPS, concurrency, RPO/RTO и retention. - **Решение M9:** файлы оператора не проходят Message Safety/AV в MVP; только MIME/size/audit, residual malware risk принят. diff --git a/modules/module-02-frontend-test-site.md b/VM1_app/documentation/module-02-frontend-test-site.md similarity index 92% rename from modules/module-02-frontend-test-site.md rename to VM1_app/documentation/module-02-frontend-test-site.md index f9afd88..0b9971f 100644 --- a/modules/module-02-frontend-test-site.md +++ b/VM1_app/documentation/module-02-frontend-test-site.md @@ -1,11 +1,11 @@ # module-02. Проектная спецификация тестового frontend-сайта > Статус: целевая спецификация реализации MVP; это проектирование, не код. -> Канонические источники: [`README.md`](README.md), [`arch-00-glossary.md`](arch-00-glossary.md), [`arch-01-system-architecture.md`](arch-01-system-architecture.md), [`arch-02-api-contracts.md`](arch-02-api-contracts.md), [`arch-03-docker-compose-blueprint.md`](arch-03-docker-compose-blueprint.md), [`arch-04-settings-and-content.md`](arch-04-settings-and-content.md), [`arch-05-agent-development-process.md`](arch-05-agent-development-process.md), [`module-01-api-backend.md`](module-01-api-backend.md). +> Канонические источники: [`README.md`](README.md), [`arch-00-glossary.md`](../../architectory/arch-00-glossary.md), [`arch-01-system-architecture.md`](../../architectory/arch-01-system-architecture.md), [`arch-02-api-contracts.md`](../../architectory/arch-02-api-contracts.md), [`arch-03-docker-compose-blueprint.md`](../../architectory/arch-03-docker-compose-blueprint.md), [`arch-04-settings-and-content.md`](../../architectory/arch-04-settings-and-content.md), [`arch-05-agent-development-process.md`](../../architectory/arch-05-agent-development-process.md), [`arch-06-service-hosting-security.md`](../../architectory/arch-06-service-hosting-security.md), [`module-01-api-backend.md`](module-01-api-backend.md). ## 1. Назначение и границы -Сайт нужен для ручной, интеграционной и E2E-проверки всех пользовательских сценариев HAN Chat через реальные публичные API. Он остаётся простым по визуальному дизайну, но функционально покрывает guest, OTP/PKCE, bootstrap, UX-сессию, чат, файлы, профиль, realtime и деградации. +Сайт нужен для ручной, интеграционной и E2E-проверки всех пользовательских сценариев HAN Chat через реальные публичные API. Он остаётся простым по визуальному дизайну, но функционально покрывает guest, OTP/PKCE, bootstrap, UX-сессию, чат, файлы, профиль, Notification Center, realtime и деградации. Сайт не реализует бизнес-решения backend, не обращается к PostgreSQL, Redis, Bitrix24 или S3 постоянными credentials и не подменяет Message Safety. Публичный вопрос отправляется обычным текстовым сообщением. @@ -33,7 +33,7 @@ frontend-test-site/ profile.tsx diagnostics.tsx src/ - api/{client,errors,public,auth,dialogs,attachments,profile}.ts + api/{client,errors,public,auth,dialogs,attachments,profile,notifications}.ts auth/{oidc,pkce,token-store,refresh-single-flight}.ts session/{ux-session,activity}.ts realtime/{socket,polling,reconcile}.ts @@ -96,7 +96,11 @@ Resend запускает новое Keycloak action, блокирует double Readonly блок «Личные данные» из `GET /me`; блок «Документы» из `/me/documents`, допускается пустой. Редактирование отсутствует. Для изменения данных — CTA в чат. Download URL запрашивается только после клика и не сохраняется. -### 5.5. Diagnostics (только non-production) +### 5.5. Notification Center + +Главная показывает bounded carousel активных G/P-уведомлений, центр — paginated список и состояния personal notifications. Публичные guest-кампании читаются без JWT; персональные read/action/hide/upload операции требуют JWT, ownership и idempotency по arch-02. CTA использует только allow-listed action types, external URL открывается безопасно, upload/download URL не сохраняются. WS-событие уведомления служит только сигналом обновить данные через REST; после reconnect выполняется reconciliation. + +### 5.6. Diagnostics (только non-production) Последние безопасные request id, HTTP status, WS state, cursor, время token expiry и UX-session id. Tokens, OTP, PII, тела сообщений и presigned URL не выводятся. diff --git a/VM1_app/documentation/module-03-nginx-vm1.md b/VM1_app/documentation/module-03-nginx-vm1.md new file mode 100644 index 0000000..b6e5493 --- /dev/null +++ b/VM1_app/documentation/module-03-nginx-vm1.md @@ -0,0 +1,214 @@ +# module-03-vm1. Nginx ВМ1 HAN Chat + +> Статус: целевая спецификация nginx на ВМ1. +> Канонический контракт (TLS/ACME, request id, internal 404, logs, reload) — [`arch-08-nginx.md`](../../architectory/arch-08-nginx.md). +> Обязательный host/container hardening baseline — [`arch-06-service-hosting-security.md`](../../architectory/arch-06-service-hosting-security.md). +> Контур ВМ2 — [`module-03-nginx-vm2.md`](../../VM2_services/documentation/module-03-nginx-vm2.md). Публичный трафик ВМ2 не проходит через этот nginx. + +## 1. Назначение и границы + +Nginx ВМ1 — публичная точка входа приложения: frontend, API, auth, Open Lines, SMS callback. Message Safety и CRM webhook на этой машине не публикуются. + +Канонический вызов Safety: `api-backend` напрямую → private `8443` nginx ВМ2. Public `/internal/` на ВМ1 всегда `404`; upstream `processing_gateway` и proxy route Safety в nginx ВМ1 запрещены. + +## 2. Routing matrix + +Порядок location критичен. Prefix `/` — последним. + +| Внешний путь | Upstream | Режим | +|---|---|---| +| `/api/` | `api-backend:8000` | REST; `/api/v1/realtime` WS | +| `/auth/` | `keycloak:8080` | OIDC/OTP, prefix/hostname согласован с issuer | +| 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 | +| `/` | static SPA либо Expo dev upstream | `try_files` fallback | + +Notification paths внутри `/api/` имеют отдельные edge-зоны. `/internal/`, `/_internal/`, Redis/OTLP/admin/status/config files → `404` (arch-08). `/bitrix/sync/webhook/*` на ВМ1 не маршрутизируется. + +## 3. Upstreams + +Именованные upstream: `api_backend`, `keycloak`, `sms_service`, `bitrix_local`, опционально `frontend_dev`. + +Upstream failures: `502/504` с безопасным body и `X-Request-ID`. Custom JSON error допустим для `/api`, но не имитирует backend domain code. Fallback в SPA запрещён. + +## 4. Listeners и TLS + +Public host ВМ1: `:80` только ACME + `308 https://$host$request_uri`; `:443 ssl http2` по arch-08. Собственный сертификат, не разделяется с ВМ2. + +## 5. WebSocket + +Только exact `location = /api/v1/realtime`: + +- `proxy_http_version 1.1`; +- `Upgrade $http_upgrade`, `Connection` через `map`; +- buffering и response cache выключены; +- read timeout больше ping interval (ориентир 75 с), send timeout bounded; +- rate limit handshake и `limit_conn` на IP; +- query `access_token` вырезается/редактируется из логов; +- subprotocol передаётся; +- при shutdown nginx позволяет grace reconnect, frontend восстанавливается polling. + +## 6. Timeouts ВМ1 + +Ориентиры arch-08 §6. Обязательно: + +- message POST read timeout ≥ `MESSAGE_SAFETY_TASK_POLL_MAX_SEC + 30s`; при default safety max 300 сек — не меньше 330 сек; +- значение из env template до startup. + +`client_max_body_size` global 8m; JSON API locations строже, где возможно. Байты вложения через nginx не идут. + +## 7. Edge rate limits ВМ1 + +Зоны `limit_req_zone` по binary remote address: + +- `auth`: `NGINX_RATE_LIMIT_AUTH`, малый burst, без большого nodelay; +- `public`: config/content, 60/min/IP; +- `api`: общий API; +- `polling`: GET messages fallback; +- `downloads`: issuance URL; +- `notifications_read`: list/counter/detail; +- `notifications_action`: read/hide/CTA/button; +- `notification_upload`: универсальные upload drafts; +- `notifications_public`: guest notifications и каталог видов; +- `bitrix_callbacks`: мягкий burst для повторов local app; +- `idgtl_callbacks`: отдельный bounded burst, учитывающий повтор каждые 5 минут в течение суток; +- `ws_connect`: handshake; +- `connections`: `limit_conn`. + +Ответ превышения — `429`, `Retry-After`, request id. OPTIONS не должен расходовать auth budget чрезмерно. Resource/FD limits учитывают WS. + +## 8. Static SPA и dev mode + +Production: + +- root `${FRONTEND_STATIC_PATH}`; +- volume `frontend-static` → `/usr/share/nginx/html:ro`; +- существующие hashed assets — `Cache-Control: public, max-age=31536000, immutable`; +- `index.html`, manifest/service worker — `no-cache` либо короткая revalidation; +- `try_files $uri $uri/ /index.html`; +- dotfiles, source maps (если не предназначены), config/env artifacts запрещены; +- API/Bitrix/auth/internal locations объявлены до SPA и никогда в неё не fallback. + +Dev: при `FRONTEND_DEV_PROXY_ENABLED=true` `/` проксируется на allow-listed `EXPO_DEV_SERVER_URL`, с WS/HMR. Этот режим запрещён при `APP_ENV=production-like|production`; startup template validator fail-fast. + +## 9. Public caching + +`GET /api/v1/public/app-config` и `/content` кэшируются только для GET/HEAD, с key `scheme+host+uri+accept-encoding` (и locale query, если контракт его использует). Backend `Cache-Control`/ETag учитываются. Базовый TTL — `security.public_cache.max_age_seconds`/3600. + +- `Set-Cookie` не кэшируется; +- Authorization request bypass cache; +- stale-if-error допускается ограниченно и маркируется `Warning`; +- mutation, auth, Bitrix, profile, dialogs, downloads и errors не кэшируются; +- cache status пишется в log, но наружу технологический header опционален. + +## 10. CORS, CSP и security headers + +CORS — exact allow-list из согласованного deploy config; application CORS остаётся последней инстанцией. Wildcard с credentials запрещён. Allowed headers: `Authorization`, `Content-Type`, `X-Request-ID`, `X-Ux-Session-Id`, `Idempotency-Key`, `traceparent`; методы соответствуют OpenAPI. Preflight получает bounded max-age. + +Для SPA: + +- CSP default-src `'self'`; +- connect-src `'self'` `https:` к разрешённому S3 endpoint и `wss:` текущего host; +- img-src `'self' data: blob:` и разрешённые signed HTTPS resources; +- object-src `'none'`, base-uri `'self'`, frame-ancestors `'none'`; +- frame-src `'none'`: инструкция `install_app` всегда открывается в новой вкладке, iframe/модалка не поддерживается; +- script-src без `unsafe-eval` production; nonce/hash при необходимости; +- style-src policy согласовать с Expo build, постепенно исключить unsafe-inline. + +Для Keycloak login endpoints под `/auth/` применяется отдельный CSP, не SPA-policy. При `KEYCLOAK_YANDEX_CAPTCHA_ENABLED=true` он точечно разрешает `https://smartcaptcha.cloud.yandex.ru` и необходимые static resources `https://yastatic.net` только в соответствующих directives; wildcard и ослабление CSP остальных `/auth/*` запрещены. При выключенной CAPTCHA эти origins отсутствуют. Env validator обязан согласовать CAPTCHA flag, CSP allow-list и ограниченный egress Keycloak. + +Также: `X-Content-Type-Options: nosniff`, `Referrer-Policy: strict-origin-when-cross-origin`, `Permissions-Policy`, frame protection через CSP, корректный COOP/CORP без поломки Keycloak redirect/S3. + +Bitrix placement может требовать embedding: для exact `/bitrix/placement` CSP `frame-ancestors` задаётся отдельным allow-list Bitrix24, а не ослабляет SPA. + +## 11. Callback i-Digital Direct + +- Только exact `location = /callbacks/idgtl/sms`; разрешён только `POST`, остальные методы отклоняются. +- Source IP allowlist — `185.203.96.7`, но значение обязательно повторно сверяется с актуальной документацией Direct перед production. При WAF/LB используется только нормализованный trusted client IP. +- TLS обязателен; cache выключен; body size ограничен под массив callback items. +- Basic `Authorization` передаётся `sms-service`, но никогда не записывается в access/error logs. URL с credentials также редактируется. +- Nginx не проверяет provider payload и не преобразует статусы; это делает `sms-service`. Ошибку upstream/DB нельзя маскировать `2xx`, иначе Direct не повторит callback. +- `/internal/sms/*` и порт sms-service наружу не публиковать. + +## 12. Health и synthetic ВМ1 + +Внутренний `/nginx-health/live` — arch-08 §8. Внешняя synthetic проверка отдельно проверяет TLS, redirect, public API, auth discovery и SMS callback route. Upstream `/health/ready` local app не публикуется без решения ops. + +## 13. Layout и Compose ВМ1 + +Каркас arch-08 §10 плюс snippets `websocket.conf`. Volume `frontend-static` только на ВМ1. + +`nginx` публикует `${NGINX_HTTP_PORT}:80`, `${NGINX_HTTPS_PORT}:443`. Private `8443` на ВМ1 нет. + +## 14. Failure behavior ВМ1 + +Дополнительно к arch-08 §12: + +- API/Keycloak upstream down: bounded 502/504, без SPA fallback. +- Safety slow: nginx ждёт message budget ≥ max+30, затем 504; backend checkpoint продолжает recovery. +- Redis down не влияет на запуск nginx; app решает degraded policy. + +Остановленный nginx ВМ1 не должен ломать приём CRM webhook на ВМ2. + +## 15. Валидация и тесты ВМ1 + +Команды acceptance (подставить `` ВМ1): + +```text +docker compose config +docker compose exec nginx nginx -t +curl -I http:/// +curl -vk https:///api/v1/public/app-config +openssl s_client -connect :443 -servername +curl -i https:///internal/safety/v2/messages/check +``` + +Ожидания: HTTP → 308; public config 200; internal Safety снаружи `404`; valid cert. + +Автоматические тесты: + +- Test::Nginx/containers для route precedence, methods, 404 internal; +- TLS scan: только 1.2/1.3, chain/hostname/expiry; +- redirect и ACME challenge; +- request-id valid/invalid/generation/propagation; +- forwarded spoof rejection; +- WS handshake, ping idle и reconnect; +- message request длительнее safety max не обрывается до budget; +- body/header limits; +- rate zones/429/Retry-After, включая notification read/action/upload/public; +- public cache HIT/MISS/bypass/no private cache; +- CSP/CORS preflight и Bitrix placement exception; +- CSP содержит `frame-src 'none'`; инструкция проверяется как новая вкладка без embedded content; +- `/internal/notifications/*` снаружи всегда `404`; +- allowed Direct callback проходит; wrong IP/method и любой `/internal/sms/*` отклоняются; Authorization отсутствует в логах; +- CRM webhook paths на ВМ1 не проксируются в `bitrix-sync`; +- upstream down/timeout, failed reload, renewal rehearsal; +- logs не содержат secrets/query tokens. + +## 16. Definition of Done ВМ1 + +Дополнительно к arch-08 §13: + +- routing matrix §2 и route precedence покрыты; +- WS работает на `/api/v1/realtime`; +- message timeout равен safety max + минимум 30 секунд; +- limits, public cache, CSP/CORS/security headers проверены; +- static production и dev proxy guard работают; +- SMS callback allow-list/method/redaction проверены; +- internal Safety/sync снаружи `404`. + +## 17. TBD ВМ1 + +- N1: доверенные WAF CIDR перед ВМ1. +- N3: нужен ли публичный health. +- N4: точный CSP Expo build. +- N5: Bitrix frame ancestor domains. +- N6: финальные burst/connection limits зон ВМ1. + +## 18. Ссылки + +- Контракт: [`arch-08-nginx.md`](../../architectory/arch-08-nginx.md). +- ВМ2: [`module-03-nginx-vm2.md`](../../VM2_services/documentation/module-03-nginx-vm2.md). +- Указатель: [`module-03-nginx.md`](module-03-nginx.md). +- API / local app / SMS / Keycloak: [`module-01-api-backend.md`](module-01-api-backend.md), [`module-06-bitrix-local-app.md`](module-06-bitrix-local-app.md), [`module-11-idgtl-sms.md`](module-11-idgtl-sms.md), [`module-08-keycloak.md`](module-08-keycloak.md). diff --git a/VM1_app/documentation/module-04-redis-vm1.md b/VM1_app/documentation/module-04-redis-vm1.md new file mode 100644 index 0000000..fb4c43c --- /dev/null +++ b/VM1_app/documentation/module-04-redis-vm1.md @@ -0,0 +1,164 @@ +# module-04-vm1. Redis ВМ1 HAN Chat + +> Статус: целевая спецификация Redis на ВМ1. +> Канонический контракт (ключи, TTL, Lua, AOF, ACL, eviction) — [`arch-09-redis.md`](../../architectory/arch-09-redis.md). +> Обязательный host/container hardening baseline — [`arch-06-service-hosting-security.md`](../../architectory/arch-06-service-hosting-security.md). +> Redis Safety ВМ2 — [`module-04-redis-vm2.md`](../../VM2_services/documentation/module-04-redis-vm2.md). Hostname Redis ВМ2 не используется. + +## 1. Назначение и границы + +Redis ВМ1 обслуживает `api-backend`: DB0 (rate/idempotency) и DB1 (realtime/coordination). OTP counters здесь нет. Message Safety cache/rate/wakeup — на ВМ2. + +Legacy DB2 на ВМ1 существует только для test stub v1 до cutover и после него удаляется. Production v2 не хранит Safety task state в этом Redis. + +Durable idempotency/outbox/checkpoint — [`module-01-api-backend.md`](module-01-api-backend.md). + +## 2. URL и ACL + +```text +REDIS_URL=redis://api_backend:@redis:6379/0 +REDIS_REALTIME_URL=redis://api_backend:@redis:6379/1 +``` + +Только на ВМ1. `api_backend` ACL: prefixes `han:api:*`, `han:rt:*`, `han:coord:*`, нужные command categories. `SELECT` запрещён. `MESSAGE_SAFETY_REDIS_URL` на ВМ1 после cutover отсутствует. + +## 3. DB0: API rate limiting + +| Key | Тип/value | TTL | +|---|---|---| +| `han:api:rl:user:{user_id}:{route_hash}:{window}` | ZSET timestamps либо counter | window + jitter | +| `han:api:rl:ip:{ip_hmac}:{route_hash}:{window}` | ZSET/counter | window + jitter | +| `han:api:rl:dialog:{dialog_id}:message:{window}` | ZSET/counter | window + jitter | +| `han:api:rl:service:{service}:{route_hash}:{window}` | counter/token bucket | window + jitter | +| `han:api:jwks:negative:{kid_hash}` | marker отрицательного lookup | 30s | + +Алгоритм — atomic Lua/function: удалить старые entries, посчитать, добавить текущий request, установить expiry, вернуть `allowed`, `remaining`, `retry_after_ms`, `reset_at`. Для fixed window `INCR` и первый `EXPIRE` выполняются в одном script, чтобы не оставить бессрочный key. + +Clock — Redis `TIME`. Route labels — bounded allow-list/hash, исключающий cardinality attack. + +## 4. DB0: idempotency + +| Key | Значение | TTL | +|---|---|---| +| `han:api:idem:{scope}:{user_id}:{key_hmac}` | HASH/MessagePack: state, fingerprint, status, sanitized response, resource id, version | 24h | +| `han:api:idemlock:{scope}:{user_id}:{key_hmac}` | random owner token | 30s + heartbeat | + +State transitions `absent → in_progress → completed`; fingerprint mismatch возвращает conflict. Создание/сравнение/lock выполняется Lua. Unlock/extend разрешены только если owner token совпадает (`compare-and-delete/expire` script). + +Response не содержит tokens, cookies, presigned URL или PII. Transient 503/504 не фиксируется как окончательный completed. PostgreSQL `idempotency_records` — durable fallback; Redis — ускоритель. При cache loss API читает durable row и прогревает key. + +## 5. DB1: realtime + +| Key/channel | Формат | TTL | +|---|---|---| +| `han:rt:conn:{connection_id}` | HASH: user_id, instance, last_seen, subscriptions_count | 90s | +| `han:rt:user:{user_id}:connections` | ZSET connection_id → heartbeat | 120s | +| `han:rt:dialog:{dialog_id}` | Pub/Sub channel | нет хранения | +| `han:rt:user:{user_id}` | Pub/Sub channel | нет хранения | + +Heartbeat атомарно обновляет connection и membership; cleanup удаляет stale ZSET entries bounded batches. Pub/Sub — at-most-once notification. Payload содержит только event id/type/entity UUID и DTO, допустимый realtime контрактом; DB остаётся source of truth. После reconnect frontend всегда делает REST reconciliation. + +Redis Streams не используются как бизнес queue. Если позже понадобится durable realtime replay, сначала меняется архитектура и выбирается PostgreSQL outbox/event broker. + +## 6. DB1: coordination locks + +| Key | TTL | +|---|---| +| `han:coord:lock:safety-recovery:{task_id}` | 30s | +| `han:coord:lock:delivery:{message_id}` | 30s | +| `han:coord:lock:settings-refresh:{instance}` | 30s | +| `han:settings:snapshot:{version}` | 5m | + +Acquire: `SET key owner NX PX ttl`; extend/release — Lua compare owner. Worker обязан опираться также на PostgreSQL row lease/`FOR UPDATE SKIP LOCKED`; Redis lock — оптимизация, не единственная защита. Fencing token рекомендуется для внешнего side effect, а уникальные DB constraints/idempotency остаются финальной защитой. + +Lock `safety-recovery` на ВМ1 относится к recovery caller/`api-backend`, не к Redis Safety ВМ2. + +## 7. Legacy DB2 stub + +До cutover test stub v1 может временно использовать DB2 ВМ1 для random task state. Этот namespace не используется production v2 и удаляется вместе со stub. + +Если stub ещё жив: Safety task TTL в DB2 должен превышать poll/recovery budget; Lua `safety task get+increment poll` допустим только здесь. После cutover keys, ACL и DB2 удаляются. + +## 8. Lua scripts ВМ1 + +Обязательные: + +- rate-limit evaluate; +- idempotency reserve/complete/conflict; +- lock release/extend; +- realtime heartbeat/cleanup membership. + +Правила хранения/тестов — arch-09 §6. + +## 9. Sizing ВМ1 + +```text +DB0 rate = peak identities × routes × active windows × bytes/key +DB0 idem = mutating requests/24h × avg sanitized record +DB1 = peak connections × connection metadata + Pub/Sub buffers +total × 1.5 allocator/fragmentation × 1.3 growth reserve +``` + +При memory pressure eviction idempotency не создаёт дубль благодаря PostgreSQL fallback. Если instances разделят (R6), DB0 idempotency может получить `noeviction`. + +## 10. Degraded behavior ВМ1 + +При Redis недоступен: + +- message send, attachment init и download URL `api-backend` fail-closed `503`, если нельзя безопасно применить лимит/idempotency; +- completed idempotency восстанавливается из PostgreSQL; +- profile/history GET могут работать под edge limits; +- public GET использует bounded local conservative limiter/cache; +- realtime cross-instance publish/coordination деградирует; REST/polling остаётся source of truth; +- internal inbox не теряется из-за Redis, так как durable receipt в PostgreSQL; +- legacy stub v1 может стать недоступным при потере своей DB2 до cutover. + +Production Safety на ВМ2 при этом продолжает PostgreSQL claim; это не runbook Redis ВМ1. + +Restore: `api-backend` прогревает idempotency по durable records, realtime восстанавливается reconnect/polling. + +## 11. Metrics ВМ1 + +Общие — arch-09 §14 и [`module-09-observability-vm1.md`](module-09-observability-vm1.md). Дополнительно: rate limit decisions, idempotency hit/conflict/fallback, Pub/Sub subscribers/output buffer. + +## 12. Тесты ВМ1 + +- ACL: `api_backend` видит только свои prefix/commands; Safety prefixes отсутствуют; +- порт 6379 недоступен с host/public network; +- rate Lua concurrency и exact Retry-After; +- idempotency same/different fingerprint, lock ownership, expiry, Redis loss + PostgreSQL fallback; +- realtime heartbeat cleanup, duplicate disconnect, Pub/Sub loss + REST recovery; +- locks expiry/late owner/fencing; +- `NOSCRIPT` reload; +- all application keys имеют TTL; +- max value/invalid serialization; +- restart with AOF/RDB, corrupted AOF rehearsal, empty restore; +- memory pressure/eviction и no duplicate business side effect; +- network partition, latency, reconnect backoff; +- logs/metrics не содержат secret/value/PII; +- после cutover DB2/stub keys отсутствуют. + +## 13. Definition of Done ВМ1 + +Дополнительно к arch-09 §15: + +- DB0/DB1 roles и prefixes реализованы; +- idempotency 24h и durable fallback доказаны; +- realtime loss восстанавливается REST; +- health/degraded policies §10 реализованы в `api-backend`; +- dashboards/alerts Redis ВМ1 готовы; +- до cutover: Safety DB2 task TTL превышает poll/recovery budget, если stub ещё включён; +- после cutover: DB2 и stub namespace удалены. + +## 14. TBD ВМ1 + +- R1/R2: maxmemory и eviction после load profile ВМ1. +- R6: момент разделения DB0/DB1 на instances. +- R3: имена credential env — arch-04. + +## 15. Ссылки + +- Контракт: [`arch-09-redis.md`](../../architectory/arch-09-redis.md). +- ВМ2: [`module-04-redis-vm2.md`](../../VM2_services/documentation/module-04-redis-vm2.md). +- Указатель: [`module-04-redis.md`](module-04-redis.md). +- API: [`module-01-api-backend.md`](module-01-api-backend.md). diff --git a/modules/module-06-bitrix-local-app.md b/VM1_app/documentation/module-06-bitrix-local-app.md similarity index 97% rename from modules/module-06-bitrix-local-app.md rename to VM1_app/documentation/module-06-bitrix-local-app.md index 7daa4f9..bc29980 100644 --- a/modules/module-06-bitrix-local-app.md +++ b/VM1_app/documentation/module-06-bitrix-local-app.md @@ -2,7 +2,7 @@ > Статус: целевая production-спецификация MVP. > Портал: `han0107.bitrix24.ru`; connector: `han_mobile_app`; Open Line: `8`. -> Источники: [`README.md`](README.md), [`arch-00-glossary.md`](arch-00-glossary.md), [`arch-01-system-architecture.md`](arch-01-system-architecture.md), [`arch-02-api-contracts.md`](arch-02-api-contracts.md), [`arch-03-docker-compose-blueprint.md`](arch-03-docker-compose-blueprint.md), [`arch-04-settings-and-content.md`](arch-04-settings-and-content.md), [`arch-05-agent-development-process.md`](arch-05-agent-development-process.md), [`module-01-api-backend.md`](module-01-api-backend.md), прототип [`../../HAN_chat/bitrix-local-app/README.md`](../../HAN_chat/bitrix-local-app/README.md) и [`../../HAN_chat/deploy/init-managed-postgres.py`](../../HAN_chat/deploy/init-managed-postgres.py). +> Источники: [`README.md`](README.md), [`arch-00-glossary.md`](../../architectory/arch-00-glossary.md), [`arch-01-system-architecture.md`](../../architectory/arch-01-system-architecture.md), [`arch-02-api-contracts.md`](../../architectory/arch-02-api-contracts.md), [`arch-03-docker-compose-blueprint.md`](../../architectory/arch-03-docker-compose-blueprint.md), [`arch-04-settings-and-content.md`](../../architectory/arch-04-settings-and-content.md), [`arch-05-agent-development-process.md`](../../architectory/arch-05-agent-development-process.md), [`arch-06-service-hosting-security.md`](../../architectory/arch-06-service-hosting-security.md), [`module-01-api-backend.md`](module-01-api-backend.md). ## 1. Назначение и приоритет @@ -36,7 +36,7 @@ https://han0107.bitrix24.ru/contact_center/connector/?ID=han_mobile_app&LINE=8 - JWT/пользовательскую авторизацию, Message Safety и App DB; - хранение истории HAN, realtime и S3; -- CRM Contact/profile sync — это будущая зона `bitrix-sync`; +- CRM Contact/profile sync — зона `bitrix-sync` на ВМ2, не этого модуля; - изменение `Dialog.status` в `han_app`; - публикацию internal API на edge. diff --git a/modules/module-08-keycloak.md b/VM1_app/documentation/module-08-keycloak.md similarity index 97% rename from modules/module-08-keycloak.md rename to VM1_app/documentation/module-08-keycloak.md index 7d9240b..d9a9908 100644 --- a/modules/module-08-keycloak.md +++ b/VM1_app/documentation/module-08-keycloak.md @@ -1,7 +1,7 @@ # module-08. Проектная спецификация `keycloak` > Статус: целевая production-спецификация OTP; mock действует до controlled rollout, real mode интегрируется только через `sms-service` по [`module-11-idgtl-sms.md`](module-11-idgtl-sms.md). -> Источники: [`README.md`](README.md), [`arch-00-glossary.md`](arch-00-glossary.md), [`arch-01-system-architecture.md`](arch-01-system-architecture.md), [`arch-02-api-contracts.md`](arch-02-api-contracts.md), [`arch-03-docker-compose-blueprint.md`](arch-03-docker-compose-blueprint.md), [`arch-04-settings-and-content.md`](arch-04-settings-and-content.md), [`arch-05-agent-development-process.md`](arch-05-agent-development-process.md), [`module-01-api-backend.md`](module-01-api-backend.md), [`module-02-frontend-test-site.md`](module-02-frontend-test-site.md), [`module-03-nginx.md`](module-03-nginx.md), [`../../HAN_chat/deploy/init-managed-postgres.py`](../../HAN_chat/deploy/init-managed-postgres.py). +> Источники: [`README.md`](README.md), [`arch-00-glossary.md`](../../architectory/arch-00-glossary.md), [`arch-01-system-architecture.md`](../../architectory/arch-01-system-architecture.md), [`arch-02-api-contracts.md`](../../architectory/arch-02-api-contracts.md), [`arch-03-docker-compose-blueprint.md`](../../architectory/arch-03-docker-compose-blueprint.md), [`arch-04-settings-and-content.md`](../../architectory/arch-04-settings-and-content.md), [`arch-05-agent-development-process.md`](../../architectory/arch-05-agent-development-process.md), [`arch-06-service-hosting-security.md`](../../architectory/arch-06-service-hosting-security.md), [`module-01-api-backend.md`](module-01-api-backend.md), [`module-02-frontend-test-site.md`](module-02-frontend-test-site.md), [`module-03-nginx-vm1.md`](module-03-nginx-vm1.md). ## 1. Назначение и границы @@ -595,7 +595,7 @@ OTEL support зависит от версии; HTTP/provider/settings bridge spa ## 23. Docker/runtime hardening - service `keycloak`, `expose: 8080` и management port только internal; -- networks `public` (только nginx access при необходимости), `backend`, `observability`; +- networks `public` (только nginx access при необходимости), `backend`, `observability`; при `KEYCLOAK_YANDEX_CAPTCHA_ENABLED=true` дополнительно ограниченная `egress` только к `smartcaptcha.cloud.yandex.ru:443` для server-side `/validate`, при `false` egress у Keycloak отсутствует; - без host `ports`; - non-root, read-only rootfs где совместимо, tmpfs для temp; - no-new-privileges/drop capabilities; diff --git a/VM1_app/documentation/module-09-observability-vm1.md b/VM1_app/documentation/module-09-observability-vm1.md new file mode 100644 index 0000000..e16fe96 --- /dev/null +++ b/VM1_app/documentation/module-09-observability-vm1.md @@ -0,0 +1,196 @@ +# module-09-vm1. Наблюдаемость ВМ1 HAN Chat + +> Статус: целевая спецификация реализации наблюдаемости на ВМ1. +> Канонический контракт (JSON-лог, redaction, sampling, Collector pipeline, SigNoz) — [`arch-07-observability.md`](../../architectory/arch-07-observability.md). Его поля, labels и `service.namespace` здесь не переопределяются. +> Контур ВМ2 в этот документ не входит: [`module-09-observability-vm2.md`](../../VM2_services/documentation/module-09-observability-vm2.md). Корреляция сквозного запроса — по `request_id` / `trace_id`. + +## 1. Назначение и границы + +Документ задаёт, **что агент ВМ1 реализует в Compose, коде, тестах и алертах этой машины**. + +ВМ1 владеет публичным edge, `api-backend`, Keycloak, `bitrix-local-app`, SMS-контуром, Redis DB0/DB1 и локальным Collector. Message Safety, `bitrix-sync`, ClamAV и Redis Safety живут на ВМ2; ВМ1 только вызывает Safety по private HTTPS и продолжает trace. + +Агент ВМ1 не добавляет scrape, дашборды и алерты сервисов ВМ2. + +## 2. Сервисы и `service.name` + +| Компонент | `service.name` | +|---|---| +| edge nginx | `nginx` | +| `api-backend` | `api-backend` | +| `bitrix-local-app` | `bitrix-local-app` | +| Keycloak | `keycloak` | +| `sms-service` | `sms-service` | +| `sms-worker` | `sms-worker` | +| Redis DB0/DB1 | `redis` | +| local Collector | `otel-collector` | + +SMS-метрики и алерты детализированы в [`module-11-idgtl-sms.md`](module-11-idgtl-sms.md); имена зарегистрированы в arch-07 §4. + +Различать экземпляр от ВМ2 через `host.name` / `service.instance.id`, не через другое `service.namespace`. + +## 3. Collector на ВМ1 + +- отдельный экземпляр в root Compose ВМ1, собственный volume `otel-queue`; +- приложения ВМ1 экспортируют OTLP только в `otel-collector:4317` этой машины; +- export в SigNoz `192.168.0.5:4317`; hostname collector ВМ2 не используется; +- pipeline, processors, limits, `otel-queue-init` и fail-open — arch-07 §3, §13, §14. + +Scrape targets ВМ1 (кроме самого Collector): Keycloak management metrics, Redis exporter приложения, nginx exporter, сервисные `/metrics` `api-backend` и `bitrix-local-app`, если они не идут OTLP. + +## 4. Instrumentation + +Правила FastAPI/HTTPX/PG/Redis/S3/workers, JSON access log nginx и hostmetrics — arch-07 §8. Ниже только покрытие ВМ1. + +### 4.1. `api-backend` + +- server spans с route template; +- child spans: PostgreSQL App DB, Redis DB0/DB1, S3, Message Safety, Open Lines / `bitrix-local-app`, JWKS; +- internal calls передают `traceparent`, `tracestate`, `X-Request-ID`; +- `ux_session_id` в JSON-логах только если передан `X-Ux-Session-Id`; +- исключить `/health/live` из traces. + +### 4.2. `bitrix-local-app` + +- server spans handler/install/placement; +- outbound Open Lines / Bitrix client spans закрываются результатом, даже если Bitrix не вернул context; +- async outbox/inbox — span link на origin, не подмена долгого worker trace. + +### 4.3. Keycloak + +Включаются management metrics/JVM/HTTP/DB pool. Custom OTP provider публикует counters send/verify/limit/settings-cache без phone labels. Login events идут в JSON/audit с masked/HMAC destination. Public OIDC synthetic проверяется отдельно. Native tracing — arch-07 O-TBD5. + +### 4.4. nginx ВМ1 + +Edge access log по arch-07 §8.4. Route class — bounded set публичных маршрутов ВМ1 (API, auth, WS, Bitrix local app, SMS callback, static). Без native OTEL module первый server span создаёт `api-backend` или соответствующий upstream. + +### 4.5. Redis приложения и PostgreSQL + +Exporter и ACL — arch-07 §8.2–8.3. Клиентские pool metrics публикует `api-backend` (и SMS, когда появится). Managed PG provider metrics — общий сигнал, не дублируется как метрика ВМ2. + +### 4.6. Host/Docker ВМ1 + +CPU, memory, disk, network, restarts/OOM, Docker daemon, clock sync — arch-07 §8.5. + +## 5. Метрики бизнес-потоков ВМ1 + +### API и auth + +- `han_http_requests_total{service,route,method,status_class}`; +- `han_http_request_duration_seconds`; +- `han_auth_bootstrap_total{outcome}`; +- `han_ux_session_start_total{reason}`; +- `han_jwks_refresh_total{outcome}`; +- `han_rate_limit_decisions_total{scope,outcome}`. + +### Open Lines / `bitrix-local-app` + +- message submitted → delivered end-to-end latency; +- local app outbound result/retry/ambiguous/DLQ; +- inbox depth/oldest age/forward retries/duplicate; +- OAuth time-to-expiry/refresh result; +- connector desired/observed state; +- Bitrix 429, circuit state, setup failure. + +### Files / S3 со стороны API + +- init/complete/promote/delete; +- presigned download issued; +- S3 dependency latency/error by operation and logical bucket. + +Quarantine object age, checksum/MIME/size reject и Safety file pipeline — спецификация ВМ2. + +### Frontend synthetic + +- public config/content; +- OIDC discovery/authorization page; +- WS handshake; +- test-user end-to-end flow в отдельной тестовой identity без реального PII. + +SMS metrics/alerts — [`module-11-idgtl-sms.md`](module-11-idgtl-sms.md) §10.3.8, после добавления в arch-07. + +UUID/user/session/dialog/task/message id не labels. + +## 6. Dashboards ВМ1 + +В SigNoz, с filter `host.name` / environment ВМ1: + +1. **nginx ingress ВМ1**: RPS, 4xx/5xx, upstream latency/status, 429, WS, TLS, cache. Без CRM webhook — это ВМ2. +2. **api-backend**: routes, DB/Redis pools, JWKS, circuits, outbox/safety backlog со стороны caller, S3. +3. **bitrix-local-app**: install/OAuth, connector, outbound/inbox/DLQ, API/Bitrix latency. +4. **Keycloak**: login/OTP/lockout, sessions/tokens, provider settings cache, JVM/DB. +5. **Redis приложения**: memory/evictions/AOF/latency/clients/keyspace. +6. **PostgreSQL/S3** в части App DB и бакетов, которыми пользуется ВМ1. + +Сквозные Executive/SLO, Business flow и Collector health — arch-07 §10; ВМ1 поставляет свои сигналы, не владеет панелями ВМ2. + +## 7. Alerts ВМ1 + +Политика SLO — arch-07 §11. Ниже alerts, которые закрывает on-call ВМ1. + +### Paging + +- edge/API 5xx >5% 5 минут; +- text delivery failure >5% 10 минут (сигнал ВМ1: accept/submit/Open Lines; Safety hop подтверждается с ВМ2); +- oldest outbox/inbox >5 минут либо DLQ >0 у `bitrix-local-app`; +- Keycloak login failures infrastructure class >10% 5 минут; +- Redis приложения unavailable, AOF error или sustained evictions; +- Collector ВМ1 exporter queue >80%, dropped/refused telemetry >0 sustained; +- TLS expiry публичного host ВМ1 <14 дней warning, <7 дней page; +- disk/OOM/restart loop ВМ1. + +### Ticket/warning + +- p95 regression 20% release-over-release на protected read / text send; +- settings/JWKS cache stale; +- OAuth local app expires <24h без успешного refresh; +- cardinality/ingest growth >2× baseline на сериях ВМ1. + +Safety mock, Safety config stale, `bitrix-sync` DLQ/credentials — не алерты репозитория ВМ1. + +## 8. Docker Compose ВМ1 + +Root Compose включает `otel-collector` по arch-07 §14. Сети приложений ВМ1: `backend` + `observability` (SMS worker — как в arch-03). + +Optional profile `observability-local` на ВМ1 по умолчанию выключен; включение — O-TBD6. + +## 9. Runbooks ВМ1 + +Общие (Collector not-ready, missing telemetry, remote outage, cardinality, PII) — arch-07 §16. + +### Высокая latency сообщения (hop ВМ1) + +1. Открыть trace по `request_id`. +2. Разделить nginx, API, Safety client span, S3, local app, Bitrix. +3. Если delay внутри Safety poll/scan — передать инцидент владельцу ВМ2, не менять collector ВМ1. +4. Проверить circuit, outbox age, DB pool и Redis приложения. +5. Не повторять ambiguous message без исходного idempotency key. +6. Следовать runbook [`module-01-api-backend.md`](module-01-api-backend.md) / [`module-06-bitrix-local-app.md`](module-06-bitrix-local-app.md). + +## 10. Definition of Done ВМ1 + +Дополнительно к arch-07 §17: + +- Collector ВМ1 validate + up в root Compose; +- инструментированы FastAPI/HTTPX/PG/Redis/S3 workers `api-backend` и `bitrix-local-app`; +- Keycloak metrics доступны; +- nginx JSON parsing и trace correlation edge ВМ1 проверены; +- Redis приложения и host/Collector metrics доступны; +- дашборды и alerts §6–§7 provisioned либо явно TBD до SigNoz packaging; +- canary secret/PII отсутствует в сигналах ВМ1; +- synthetic: public config, OIDC, WS handshake; +- request с `X-Request-ID` находится в nginx ВМ1, API и client span Safety; продолжение на ВМ2 не блокирует DoD ВМ1, но сквозной gate — [`arch-10-deployment.md`](../../architectory/arch-10-deployment.md) §11 / [`module-10-deployment-vm1.md`](module-10-deployment-vm1.md). + +## 11. TBD и конфликты, принадлежащие ВМ1 + +- O-TBD6: нужен ли local observability profile на ВМ1. +- O-TBD5 в части Keycloak native tracing и nginx OTEL module pinned image ВМ1. +- Production SLO Keycloak/API остаются initial ops policy, пока product owner не утвердил O-TBD2. + +## 12. Ссылки + +- Контракт: [`arch-07-observability.md`](../../architectory/arch-07-observability.md). +- ВМ2: [`module-09-observability-vm2.md`](../../VM2_services/documentation/module-09-observability-vm2.md). +- Указатель: [`module-09-observability.md`](module-09-observability.md). +- Edge nginx: [`module-03-nginx-vm1.md`](module-03-nginx-vm1.md). +- Деплой: [`module-10-deployment-vm1.md`](module-10-deployment-vm1.md). diff --git a/VM1_app/documentation/module-10-deployment-vm1.md b/VM1_app/documentation/module-10-deployment-vm1.md new file mode 100644 index 0000000..fd3eace --- /dev/null +++ b/VM1_app/documentation/module-10-deployment-vm1.md @@ -0,0 +1,213 @@ +# module-10-vm1. Runbook развёртывания ВМ1 HAN Chat + +> Статус: целевой runbook репозитория ВМ1. +> Общий контракт (VPC/SG, PG, S3, роли `deploy`, TLS процедура, порядок cutover) — [`arch-10-deployment.md`](../../architectory/arch-10-deployment.md). +> ВМ2 — [`module-10-deployment-vm2.md`](../../VM2_services/documentation/module-10-deployment-vm2.md). Не переносить команды ВМ2 и не шарить Compose/secrets. + +## 1. Границы + +ВМ1 владеет edge nginx `80/443`, `api-backend`, Keycloak, SMS, `bitrix-local-app`, Redis DB0/DB1, Collector. После Safety cutover local Safety/Redis DB2 отсутствуют; `MESSAGE_SAFETY_URL` — private HTTPS ВМ2. + +`` / `` — репозиторий ВМ1. Host ACME — ``. + +## 2. Sizing ВМ1 + +Final sizing — D-TBD2. Disk после pull/build ≥30% free. `PUBLIC_DOCKER_PORTS=80,443`. + +## 3. Hardening + +По arch-10 §5 / arch-06. Пример: + +```bash +sudo DEPLOY_USER=deploy \ + DEPLOY_DIR=/opt/han-chat \ + SSH_PORT= \ + SWAP_SIZE_GB=4 \ + PUBLIC_DOCKER_PORTS=80,443 \ + ./deploy/setup-vm-han-chat.sh +``` + +Gate 2 — arch-10. Break-glass вне VM. + +## 4. Release layout и `.env` ВМ1 + +Checkout exact SHA в `/opt/han-chat/backend`. Структура: root Compose, `nginx`, `keycloak`, `redis`, `observability`, frontend artifact. + +Обязательные группы секретов/config ВМ1: + +- `APP_ENV`, release, log level; +- private PG host/port/database и TLS CA; runtime DSN в secret backend; +- Redis ACL URLs DB0/DB1 только в secret backend; после cutover DB2 нет; +- public web/API/auth URLs; +- Keycloak realm/audience/hostname/bootstrap/provider secrets; +- SMS DB URL, парные Keycloak↔SMS tokens, Direct `TOKEN_1`, callback credentials; +- paired service tokens arch-02; +- Bitrix local app client/application/encryption secrets (не CRM sync webhook ВМ2); +- S3 API credentials (не Safety read-only key ВМ2); +- OTEL exporter secrets; +- nginx/TLS/rate limits; +- frontend public build values; +- после cutover: `MESSAGE_SAFETY_URL=https://:8443` и `MESSAGE_SAFETY_CA_HOST_PATH`. + +Пары: `BITRIX_LOCAL_APP_INTERNAL_TOKEN == BITRIX_INTERNAL_API_TOKEN`, `BITRIX_API_FORWARD_TOKEN == BITRIX_API_INBOX_TOKEN`, `KEYCLOAK_SMS_SERVICE_TOKEN == SMS_SERVICE_TOKEN`. + +`FRONTEND_DEV_PROXY_ENABLED=false`. Safety timeout согласован с nginx ВМ1. Validation — arch-10 §8; `validate-env` в этом репозитории. + +## 5. Images и frontend + +Pull или build без production secrets. Frontend: + +```bash +cd +npm ci +npm run test +npx expo export --platform web +``` + +Artifact в versioned `frontend-static`. Build env — только public URL/realm/client id. Secret scanner: нет service tokens/mock OTP/S3 keys. + +Image/frontend gate: images по digest; static без secrets; nginx image с request-id/TLS; disk >30% free. + +## 6. Root Compose ВМ1 + +Сервисы: edge `nginx`, `api-backend`, `keycloak`, `sms-service`, `sms-worker`, `bitrix-local-app`, Redis DB0/DB1, local `otel-collector`. После cutover — без `message-safety` и Redis DB2. + +Networks: `public`, `backend`, `egress` (`sms-worker`; Keycloak входит только при `KEYCLOAK_YANDEX_CAPTCHA_ENABLED=true` и только с destination allow-list SmartCaptcha API), `observability`. `sms-service` без egress при отдельном worker. + +Volumes: `redis-data`, ACME, `frontend-static`, `otel-queue` + `otel-queue-init`. Единственные published mappings — nginx 80/443. + +Compose gate — arch-10 применительно к этому Compose. + +## 7. TLS ВМ1 + +Arch-10 §9 / arch-08, `-d --cert-name `. Private `8443` на ВМ1 нет. + +## 8. Миграции и seed ВМ1 + +Preflight/upgrade: + +```bash +cd +docker compose run --rm api-backend alembic current +docker compose run --rm bitrix-local-app alembic current +# PITR marker, затем: +docker compose run --rm api-backend alembic upgrade head +docker compose run --rm bitrix-local-app alembic upgrade head +``` + +Shared `han_app.sync_queue` мигрирует api-backend **до** sync cutover на ВМ2, но grants `bitrix_sync_user` — после обеих migrations (см. runbook ВМ2). Seed `app_settings` idempotent из этого репозитория. + +SMS schema/migrations выполняет `sms-service` на ВМ1. + +### Controlled rollout real SMS + +1. seed `otp.phone.*`; +2. schema/role `sms`, migrations, seed `sms_setting`/active `auth_otp`; +3. test env с mock Direct; +4. production Direct `TOKEN_1`, sender, template, callback credentials; +5. записать `` из `sms-worker`; +6. deploy `sms-service`/worker и callback route nginx ВМ1, `KEYCLOAK_OTP_MOCK_ENABLED=true`; +7. Keycloak expand migration/SPI; +8. provider smoke на ``; +9. только после evidence — `KEYCLOAK_OTP_MOCK_ENABLED=false`; +10. проверить durable order, resend, limits. + +Rollback SMS: вернуть Keycloak в mock; не удалять schema/journal. Production cutover запрещён при placeholder или нестабильном egress IP. + +## 9. Keycloak bootstrap + +```bash +cd +docker compose up -d keycloak +``` + +Bootstrap admin только на первый запуск, затем MFA named admin и удаление bootstrap. Realm: public client PKCE S256, issuer `https:///auth/realms/han-chat`, без `--import-realm` на живой production без diff. Keycloak gate — discovery/JWKS HTTPS, OTP fail-closed, settings bridge. + +## 10. Ordered startup ВМ1 + +После готовности ВМ2 (arch-10 §10 шаги 1–3): + +1. Redis ВМ1, `otel-queue-init`, Collector; +2. API, SMS, Keycloak, local app; +3. edge nginx последним; после readiness — `nginx -t -c /tmp/nginx.conf` и HUP. +4. `MESSAGE_SAFETY_URL` переключается на ВМ2 **только** после cutover gates runbook ВМ2 и legacy gate §14. + +Не использовать host ports для health curl. Expected: Redis `PONG`; Keycloak ready; Collector health; API core DB/Redis/JWKS/settings ready. S3/Safety/Open Lines могут быть `degraded` без снятия read API из readiness, но send path при недоступном Safety остаётся fail-closed. Local app до install может быть `portal_not_installed`. + +## 11. Bitrix24 local app и Open Lines + +Install/handler/placement URL на `https:///bitrix/...`. Canonical internal path `/internal/openlines/v1/*`, не prototype `/bitrix-internal/*`. Open Lines gate: connector line 8, outbound once, operator reply, duplicate callback безопасен. + +CRM webhook robots **не** настраиваются на ВМ1. + +## 12. Public smoke ВМ1 + +```bash +curl -I http:/// +curl -fsS https:///api/v1/public/app-config +curl -fsS https:///api/v1/public/content +curl -fsS https:///auth/realms/han-chat/.well-known/openid-configuration +curl -i https:///internal/safety/v2/messages/check +openssl s_client -connect :443 -servername +``` + +Expected: 308; public 200; discovery 200; internal 404; valid cert. + +Далее: guest content; 401 без JWT; consent → OTP → PKCE; bootstrap без phone body; `ux_session_id`; silent refresh; logout; wrong OTP. Real SMS mode — по §8. + +Safety E2E **со стороны caller** (правила stub до v2 cutover): `ф` → `422 message_blocked`; allow path; timeout 503/504 без duplicate. Статус Safety `stub` не заменяет production AV. + +Files: presigned PUT quarantine, promote/deny, owner-only download, нет URL в logs. Realtime/ownership/idempotency/429 — module-01. + +Сквозной first-send до Open Lines требует готовую ВМ2; system end-to-end gate закрывается по arch-10 после обоих runbook. + +## 13. Observability ВМ1 + +[`module-09-observability-vm1.md`](module-09-observability-vm1.md) + arch-07. Сквозной `X-Request-ID` до Safety span — совместно с ВМ2. + +## 14. Legacy gate перед cutover Safety + +Перед `MESSAGE_SAFETY_URL` на ВМ2: + +1. validator принимает только `https://:8443`, требует CA path, запрещает Docker hostname и plaintext; +2. internal CA root-owned; read-test UID `api-backend`, negative посторонний UID; +3. local `message-safety`, Redis DB2 и local rules-version env удалены из Compose/validator; +4. root-owned stack unit; `deploy` не в `docker`; +5. `DOCKER-USER` counters через `conntrack --ctorigdstport` после Docker restart и reboot; +6. images digest; rollback по compatible digests; +7. ordered startup этого runbook, не legacy `docker compose up -d`; +8. `han-secrets` и firewall oneshot явно перезапущены; TLS renew success — пустой stderr. + +Ни старый single-VM guide, ни успешный stub Compose не являются evidence. Rollback caller — предыдущий immutable release ВМ1. Уже созданные v2 tasks не down-migrate. + +## 15. Rollback, ops, incidents ВМ1 + +Rollback application-only: previous digests, без Alembic downgrade; при SMS incident — mock OTP, сохранить journal. Redis restore — clean instance, прогрев idempotency из PG ([`module-04-redis-vm1.md`](module-04-redis-vm1.md)). Keycloak restore — проверить issuer/JWKS/PKCE/OTP. + +Routine: health, PG/TLS/disk/OTEL/Redis, Keycloak signing, Bitrix connector desired/observed. + +Incident triage — arch-10 команды в `` ВМ1. Типовое: API 503 (DB/Redis/JWKS/Safety circuit); send timeout — не новый idempotency key; Redis loss — polling. + +Потеря ВМ1: provision в той же VPC, restore secrets из vault, existing PG/S3, TLS ``, Bitrix local app/callbacks verify. Не пересоздавать ВМ2. + +## 16. Definition of Done ВМ1 + +Дополнительно к arch-10 §12: + +- Compose/nginx/Redis/Collector ВМ1 прошли профильные compose/nginx gates; +- Keycloak realm/provider/PKCE/OTP готов; +- SMS либо mock с accepted risk, либо real mode после §8; +- Bitrix connector line 8 проверен; +- auth/text/file/realtime E2E caller-side зелёный; +- observability ВМ1 + redaction; +- после cutover — legacy gate §14 закрыт. + +## 17. TBD ВМ1 + +D-TBD1, D-TBD2 (VM1 sizing/SLO), D-TBD8 CLI, D-TBD9 Keycloak admin VPN. + +## 18. Ссылки + +- Контракт: [`arch-10-deployment.md`](../../architectory/arch-10-deployment.md). +- ВМ2: [`module-10-deployment-vm2.md`](../../VM2_services/documentation/module-10-deployment-vm2.md). +- Указатель: [`module-10-deployment-runbook.md`](module-10-deployment-runbook.md). diff --git a/modules/module-11-idgtl-sms.md b/VM1_app/documentation/module-11-idgtl-sms.md similarity index 97% rename from modules/module-11-idgtl-sms.md rename to VM1_app/documentation/module-11-idgtl-sms.md index a32e9c1..d50c158 100644 --- a/modules/module-11-idgtl-sms.md +++ b/VM1_app/documentation/module-11-idgtl-sms.md @@ -3,9 +3,9 @@ > Статус: целевая проектная спецификация post-MVP (закрывает K-TBD8 / бэклог «интеграция с SMS-провайдером»). > Реализация отсутствует. Документ задаёт обязательные контракты для разработки `sms-service` и доработки Keycloak. > Источники провайдера: [Отправка SMS](https://api.docs.direct.i-dgtl.ru/messages/sms-sending/), [Авторизация](https://api.docs.direct.i-dgtl.ru/authorization/), [Callback](https://api.docs.direct.i-dgtl.ru/messages/callback/). -> Смежные: [`module-08-keycloak.md`](module-08-keycloak.md), [`arch-01`](../architectory/arch-01-system-architecture.md), [`arch-02`](../architectory/arch-02-api-contracts.md), [`arch-04`](../architectory/arch-04-settings-and-content.md). +> Смежные: [`module-08-keycloak.md`](module-08-keycloak.md), [`arch-01`](../../architectory/arch-01-system-architecture.md), [`arch-02`](../../architectory/arch-02-api-contracts.md), [`arch-04`](../../architectory/arch-04-settings-and-content.md), [`arch-06`](../../architectory/arch-06-service-hosting-security.md). -**Критерий применимости:** до синхронизации `arch-00`…`arch-04`, `module-08`, Compose и `.env.example` настоящий документ имеет приоритет только как спецификация нового модуля, но не изменяет действующий mock-only контур. +**Критерий применимости:** документ описывает target real-SMS rollout, который остаётся post-MVP backlog до реализации и закрытия gates. Текущее as-is состояние до cutover — `KEYCLOAK_OTP_MOCK_ENABLED=true`. При конфликте приоритет всегда у architectory/README; настоящий модуль не переопределяет действующий mock-only runtime сам по себе. ## 1. Разделение ответственности @@ -591,7 +591,7 @@ Alerting: 401/402 у Direct, contract violation, любой `uncertain`, рос - networks: `backend`, `egress`, `observability`; - `expose: 8080`, без host `ports`; - managed PostgreSQL schema `sms`, роль только `sms_user`; -- Keycloak остаётся без `egress`: он видит только `sms-service` по сети `backend`; +- Keycloak видит `sms-service` по сети `backend`; при включённой Yandex SmartCaptcha получает отдельный ограниченный egress только к SmartCaptcha API, а при выключенной CAPTCHA остаётся без egress; - root nginx маршрутизирует только точный публичный `POST /callbacks/idgtl/sms` в `sms-service`; `/internal/sms/*` наружу блокируется; - callback location: HTTPS, IP allowlist, request body limit, без access-log Authorization; - зависимости запуска не должны образовывать цикл: Keycloak может стартовать при недоступном `sms-service`; недоступность блокирует только создание нового real-mode заказа, но не verify уже активного challenge. @@ -728,7 +728,7 @@ backend/sms-service/ - `sms-service` — internal HTTP API/callback receiver; - `sms-worker` — background sender из того же image либо обязательный worker process внутри `sms-service`; -- `sms-service`: networks `backend`, `egress`, `observability`, `expose: 8080`, без `ports`; +- `sms-service`: networks `backend`, `observability`, `expose: 8080`, без `ports`; `egress` добавляется только в совмещённом callback+worker process; - отдельный `sms-worker`: networks `egress`, `observability`, без published/exposed port; - оба процесса используют `SMS_DATABASE_URL`; только worker получает `IDGTL_SMS_API_KEY`; - callback credentials получают `sms-service` для проверки и `sms-worker` для формирования callback URL в запросе Direct; Keycloak получает только `KEYCLOAK_SMS_SERVICE_TOKEN`; @@ -744,6 +744,8 @@ backend/sms-service/ #### 10.3.6. `module-03-nginx` +Требования относятся к [`module-03-nginx-vm1.md`](module-03-nginx-vm1.md) и общему контракту [`arch-08-nginx.md`](../../architectory/arch-08-nginx.md): + - добавить точный public route `POST /callbacks/idgtl/sms` → `sms-service:8080`; - остальные методы на callback path отклонять; - source IP allowlist Direct, учитывая только trusted proxy chain; @@ -763,7 +765,7 @@ backend/sms-service/ #### 10.3.8. `module-09-observability` -- добавить metrics/alerts из §9 для `sms-service` и `sms-worker`; +- добавить metrics/alerts в [`module-09-observability-vm1.md`](module-09-observability-vm1.md) и имя сервиса в реестр [`arch-07-observability.md`](../../architectory/arch-07-observability.md) §4 для `sms-service` и `sms-worker`; - dashboard: pending age, send outcomes, provider latency, callback lag, uncertain, journal growth; - traces: Keycloak order span → sms-service DB commit; worker → Direct отдельным trace/span с correlation через `sms_message_id`; - настроить redaction OTP, body, phone, Authorization, API key и callback credentials; @@ -771,7 +773,7 @@ backend/sms-service/ #### 10.3.9. `module-10-deployment-runbook` и `deploy-steps.md` -Зафиксировать rollout: +Зафиксировать rollout в [`module-10-deployment-vm1.md`](module-10-deployment-vm1.md) и контракте [`arch-10-deployment.md`](../../architectory/arch-10-deployment.md): 1. применить App DB seed новых OTP settings; 2. создать schema/role `sms`, применить migrations и seed; diff --git a/figma/ATTRIBUTIONS.md b/VM1_app/figma/ATTRIBUTIONS.md similarity index 100% rename from figma/ATTRIBUTIONS.md rename to VM1_app/figma/ATTRIBUTIONS.md diff --git a/figma/Main page specification.zip b/VM1_app/figma/Main page specification.zip similarity index 100% rename from figma/Main page specification.zip rename to VM1_app/figma/Main page specification.zip diff --git a/figma/README.md b/VM1_app/figma/README.md similarity index 100% rename from figma/README.md rename to VM1_app/figma/README.md diff --git a/figma/default_shadcn_theme.css b/VM1_app/figma/default_shadcn_theme.css similarity index 100% rename from figma/default_shadcn_theme.css rename to VM1_app/figma/default_shadcn_theme.css diff --git a/figma/guidelines/Guidelines.md b/VM1_app/figma/guidelines/Guidelines.md similarity index 100% rename from figma/guidelines/Guidelines.md rename to VM1_app/figma/guidelines/Guidelines.md diff --git a/figma/index.html b/VM1_app/figma/index.html similarity index 100% rename from figma/index.html rename to VM1_app/figma/index.html diff --git a/figma/package.json b/VM1_app/figma/package.json similarity index 100% rename from figma/package.json rename to VM1_app/figma/package.json diff --git a/figma/pnpm-workspace.yaml b/VM1_app/figma/pnpm-workspace.yaml similarity index 100% rename from figma/pnpm-workspace.yaml rename to VM1_app/figma/pnpm-workspace.yaml diff --git a/figma/postcss.config.mjs b/VM1_app/figma/postcss.config.mjs similarity index 100% rename from figma/postcss.config.mjs rename to VM1_app/figma/postcss.config.mjs diff --git a/figma/src/app/App.tsx b/VM1_app/figma/src/app/App.tsx similarity index 100% rename from figma/src/app/App.tsx rename to VM1_app/figma/src/app/App.tsx diff --git a/figma/src/app/components/ChatInput.tsx b/VM1_app/figma/src/app/components/ChatInput.tsx similarity index 100% rename from figma/src/app/components/ChatInput.tsx rename to VM1_app/figma/src/app/components/ChatInput.tsx diff --git a/figma/src/app/components/HanLogo.tsx b/VM1_app/figma/src/app/components/HanLogo.tsx similarity index 100% rename from figma/src/app/components/HanLogo.tsx rename to VM1_app/figma/src/app/components/HanLogo.tsx diff --git a/figma/src/app/components/Header.tsx b/VM1_app/figma/src/app/components/Header.tsx similarity index 100% rename from figma/src/app/components/Header.tsx rename to VM1_app/figma/src/app/components/Header.tsx diff --git a/figma/src/app/components/Notifications.tsx b/VM1_app/figma/src/app/components/Notifications.tsx similarity index 100% rename from figma/src/app/components/Notifications.tsx rename to VM1_app/figma/src/app/components/Notifications.tsx diff --git a/figma/src/app/components/PopularQuestions.tsx b/VM1_app/figma/src/app/components/PopularQuestions.tsx similarity index 100% rename from figma/src/app/components/PopularQuestions.tsx rename to VM1_app/figma/src/app/components/PopularQuestions.tsx diff --git a/figma/src/app/components/QuickActions.tsx b/VM1_app/figma/src/app/components/QuickActions.tsx similarity index 100% rename from figma/src/app/components/QuickActions.tsx rename to VM1_app/figma/src/app/components/QuickActions.tsx diff --git a/figma/src/app/components/figma/ImageWithFallback.tsx b/VM1_app/figma/src/app/components/figma/ImageWithFallback.tsx similarity index 100% rename from figma/src/app/components/figma/ImageWithFallback.tsx rename to VM1_app/figma/src/app/components/figma/ImageWithFallback.tsx diff --git a/figma/src/app/components/ui/accordion.tsx b/VM1_app/figma/src/app/components/ui/accordion.tsx similarity index 100% rename from figma/src/app/components/ui/accordion.tsx rename to VM1_app/figma/src/app/components/ui/accordion.tsx diff --git a/figma/src/app/components/ui/alert-dialog.tsx b/VM1_app/figma/src/app/components/ui/alert-dialog.tsx similarity index 100% rename from figma/src/app/components/ui/alert-dialog.tsx rename to VM1_app/figma/src/app/components/ui/alert-dialog.tsx diff --git a/figma/src/app/components/ui/alert.tsx b/VM1_app/figma/src/app/components/ui/alert.tsx similarity index 100% rename from figma/src/app/components/ui/alert.tsx rename to VM1_app/figma/src/app/components/ui/alert.tsx diff --git a/figma/src/app/components/ui/aspect-ratio.tsx b/VM1_app/figma/src/app/components/ui/aspect-ratio.tsx similarity index 100% rename from figma/src/app/components/ui/aspect-ratio.tsx rename to VM1_app/figma/src/app/components/ui/aspect-ratio.tsx diff --git a/figma/src/app/components/ui/avatar.tsx b/VM1_app/figma/src/app/components/ui/avatar.tsx similarity index 100% rename from figma/src/app/components/ui/avatar.tsx rename to VM1_app/figma/src/app/components/ui/avatar.tsx diff --git a/figma/src/app/components/ui/badge.tsx b/VM1_app/figma/src/app/components/ui/badge.tsx similarity index 100% rename from figma/src/app/components/ui/badge.tsx rename to VM1_app/figma/src/app/components/ui/badge.tsx diff --git a/figma/src/app/components/ui/breadcrumb.tsx b/VM1_app/figma/src/app/components/ui/breadcrumb.tsx similarity index 100% rename from figma/src/app/components/ui/breadcrumb.tsx rename to VM1_app/figma/src/app/components/ui/breadcrumb.tsx diff --git a/figma/src/app/components/ui/button.tsx b/VM1_app/figma/src/app/components/ui/button.tsx similarity index 100% rename from figma/src/app/components/ui/button.tsx rename to VM1_app/figma/src/app/components/ui/button.tsx diff --git a/figma/src/app/components/ui/calendar.tsx b/VM1_app/figma/src/app/components/ui/calendar.tsx similarity index 100% rename from figma/src/app/components/ui/calendar.tsx rename to VM1_app/figma/src/app/components/ui/calendar.tsx diff --git a/figma/src/app/components/ui/card.tsx b/VM1_app/figma/src/app/components/ui/card.tsx similarity index 100% rename from figma/src/app/components/ui/card.tsx rename to VM1_app/figma/src/app/components/ui/card.tsx diff --git a/figma/src/app/components/ui/carousel.tsx b/VM1_app/figma/src/app/components/ui/carousel.tsx similarity index 100% rename from figma/src/app/components/ui/carousel.tsx rename to VM1_app/figma/src/app/components/ui/carousel.tsx diff --git a/figma/src/app/components/ui/chart.tsx b/VM1_app/figma/src/app/components/ui/chart.tsx similarity index 100% rename from figma/src/app/components/ui/chart.tsx rename to VM1_app/figma/src/app/components/ui/chart.tsx diff --git a/figma/src/app/components/ui/checkbox.tsx b/VM1_app/figma/src/app/components/ui/checkbox.tsx similarity index 100% rename from figma/src/app/components/ui/checkbox.tsx rename to VM1_app/figma/src/app/components/ui/checkbox.tsx diff --git a/figma/src/app/components/ui/collapsible.tsx b/VM1_app/figma/src/app/components/ui/collapsible.tsx similarity index 100% rename from figma/src/app/components/ui/collapsible.tsx rename to VM1_app/figma/src/app/components/ui/collapsible.tsx diff --git a/figma/src/app/components/ui/command.tsx b/VM1_app/figma/src/app/components/ui/command.tsx similarity index 100% rename from figma/src/app/components/ui/command.tsx rename to VM1_app/figma/src/app/components/ui/command.tsx diff --git a/figma/src/app/components/ui/context-menu.tsx b/VM1_app/figma/src/app/components/ui/context-menu.tsx similarity index 100% rename from figma/src/app/components/ui/context-menu.tsx rename to VM1_app/figma/src/app/components/ui/context-menu.tsx diff --git a/figma/src/app/components/ui/dialog.tsx b/VM1_app/figma/src/app/components/ui/dialog.tsx similarity index 100% rename from figma/src/app/components/ui/dialog.tsx rename to VM1_app/figma/src/app/components/ui/dialog.tsx diff --git a/figma/src/app/components/ui/drawer.tsx b/VM1_app/figma/src/app/components/ui/drawer.tsx similarity index 100% rename from figma/src/app/components/ui/drawer.tsx rename to VM1_app/figma/src/app/components/ui/drawer.tsx diff --git a/figma/src/app/components/ui/dropdown-menu.tsx b/VM1_app/figma/src/app/components/ui/dropdown-menu.tsx similarity index 100% rename from figma/src/app/components/ui/dropdown-menu.tsx rename to VM1_app/figma/src/app/components/ui/dropdown-menu.tsx diff --git a/figma/src/app/components/ui/form.tsx b/VM1_app/figma/src/app/components/ui/form.tsx similarity index 100% rename from figma/src/app/components/ui/form.tsx rename to VM1_app/figma/src/app/components/ui/form.tsx diff --git a/figma/src/app/components/ui/hover-card.tsx b/VM1_app/figma/src/app/components/ui/hover-card.tsx similarity index 100% rename from figma/src/app/components/ui/hover-card.tsx rename to VM1_app/figma/src/app/components/ui/hover-card.tsx diff --git a/figma/src/app/components/ui/input-otp.tsx b/VM1_app/figma/src/app/components/ui/input-otp.tsx similarity index 100% rename from figma/src/app/components/ui/input-otp.tsx rename to VM1_app/figma/src/app/components/ui/input-otp.tsx diff --git a/figma/src/app/components/ui/input.tsx b/VM1_app/figma/src/app/components/ui/input.tsx similarity index 100% rename from figma/src/app/components/ui/input.tsx rename to VM1_app/figma/src/app/components/ui/input.tsx diff --git a/figma/src/app/components/ui/label.tsx b/VM1_app/figma/src/app/components/ui/label.tsx similarity index 100% rename from figma/src/app/components/ui/label.tsx rename to VM1_app/figma/src/app/components/ui/label.tsx diff --git a/figma/src/app/components/ui/menubar.tsx b/VM1_app/figma/src/app/components/ui/menubar.tsx similarity index 100% rename from figma/src/app/components/ui/menubar.tsx rename to VM1_app/figma/src/app/components/ui/menubar.tsx diff --git a/figma/src/app/components/ui/navigation-menu.tsx b/VM1_app/figma/src/app/components/ui/navigation-menu.tsx similarity index 100% rename from figma/src/app/components/ui/navigation-menu.tsx rename to VM1_app/figma/src/app/components/ui/navigation-menu.tsx diff --git a/figma/src/app/components/ui/pagination.tsx b/VM1_app/figma/src/app/components/ui/pagination.tsx similarity index 100% rename from figma/src/app/components/ui/pagination.tsx rename to VM1_app/figma/src/app/components/ui/pagination.tsx diff --git a/figma/src/app/components/ui/popover.tsx b/VM1_app/figma/src/app/components/ui/popover.tsx similarity index 100% rename from figma/src/app/components/ui/popover.tsx rename to VM1_app/figma/src/app/components/ui/popover.tsx diff --git a/figma/src/app/components/ui/progress.tsx b/VM1_app/figma/src/app/components/ui/progress.tsx similarity index 100% rename from figma/src/app/components/ui/progress.tsx rename to VM1_app/figma/src/app/components/ui/progress.tsx diff --git a/figma/src/app/components/ui/radio-group.tsx b/VM1_app/figma/src/app/components/ui/radio-group.tsx similarity index 100% rename from figma/src/app/components/ui/radio-group.tsx rename to VM1_app/figma/src/app/components/ui/radio-group.tsx diff --git a/figma/src/app/components/ui/resizable.tsx b/VM1_app/figma/src/app/components/ui/resizable.tsx similarity index 100% rename from figma/src/app/components/ui/resizable.tsx rename to VM1_app/figma/src/app/components/ui/resizable.tsx diff --git a/figma/src/app/components/ui/scroll-area.tsx b/VM1_app/figma/src/app/components/ui/scroll-area.tsx similarity index 100% rename from figma/src/app/components/ui/scroll-area.tsx rename to VM1_app/figma/src/app/components/ui/scroll-area.tsx diff --git a/figma/src/app/components/ui/select.tsx b/VM1_app/figma/src/app/components/ui/select.tsx similarity index 100% rename from figma/src/app/components/ui/select.tsx rename to VM1_app/figma/src/app/components/ui/select.tsx diff --git a/figma/src/app/components/ui/separator.tsx b/VM1_app/figma/src/app/components/ui/separator.tsx similarity index 100% rename from figma/src/app/components/ui/separator.tsx rename to VM1_app/figma/src/app/components/ui/separator.tsx diff --git a/figma/src/app/components/ui/sheet.tsx b/VM1_app/figma/src/app/components/ui/sheet.tsx similarity index 100% rename from figma/src/app/components/ui/sheet.tsx rename to VM1_app/figma/src/app/components/ui/sheet.tsx diff --git a/figma/src/app/components/ui/sidebar.tsx b/VM1_app/figma/src/app/components/ui/sidebar.tsx similarity index 100% rename from figma/src/app/components/ui/sidebar.tsx rename to VM1_app/figma/src/app/components/ui/sidebar.tsx diff --git a/figma/src/app/components/ui/skeleton.tsx b/VM1_app/figma/src/app/components/ui/skeleton.tsx similarity index 100% rename from figma/src/app/components/ui/skeleton.tsx rename to VM1_app/figma/src/app/components/ui/skeleton.tsx diff --git a/figma/src/app/components/ui/slider.tsx b/VM1_app/figma/src/app/components/ui/slider.tsx similarity index 100% rename from figma/src/app/components/ui/slider.tsx rename to VM1_app/figma/src/app/components/ui/slider.tsx diff --git a/figma/src/app/components/ui/sonner.tsx b/VM1_app/figma/src/app/components/ui/sonner.tsx similarity index 100% rename from figma/src/app/components/ui/sonner.tsx rename to VM1_app/figma/src/app/components/ui/sonner.tsx diff --git a/figma/src/app/components/ui/switch.tsx b/VM1_app/figma/src/app/components/ui/switch.tsx similarity index 100% rename from figma/src/app/components/ui/switch.tsx rename to VM1_app/figma/src/app/components/ui/switch.tsx diff --git a/figma/src/app/components/ui/table.tsx b/VM1_app/figma/src/app/components/ui/table.tsx similarity index 100% rename from figma/src/app/components/ui/table.tsx rename to VM1_app/figma/src/app/components/ui/table.tsx diff --git a/figma/src/app/components/ui/tabs.tsx b/VM1_app/figma/src/app/components/ui/tabs.tsx similarity index 100% rename from figma/src/app/components/ui/tabs.tsx rename to VM1_app/figma/src/app/components/ui/tabs.tsx diff --git a/figma/src/app/components/ui/textarea.tsx b/VM1_app/figma/src/app/components/ui/textarea.tsx similarity index 100% rename from figma/src/app/components/ui/textarea.tsx rename to VM1_app/figma/src/app/components/ui/textarea.tsx diff --git a/figma/src/app/components/ui/toggle-group.tsx b/VM1_app/figma/src/app/components/ui/toggle-group.tsx similarity index 100% rename from figma/src/app/components/ui/toggle-group.tsx rename to VM1_app/figma/src/app/components/ui/toggle-group.tsx diff --git a/figma/src/app/components/ui/toggle.tsx b/VM1_app/figma/src/app/components/ui/toggle.tsx similarity index 100% rename from figma/src/app/components/ui/toggle.tsx rename to VM1_app/figma/src/app/components/ui/toggle.tsx diff --git a/figma/src/app/components/ui/tooltip.tsx b/VM1_app/figma/src/app/components/ui/tooltip.tsx similarity index 100% rename from figma/src/app/components/ui/tooltip.tsx rename to VM1_app/figma/src/app/components/ui/tooltip.tsx diff --git a/figma/src/app/components/ui/use-mobile.ts b/VM1_app/figma/src/app/components/ui/use-mobile.ts similarity index 100% rename from figma/src/app/components/ui/use-mobile.ts rename to VM1_app/figma/src/app/components/ui/use-mobile.ts diff --git a/figma/src/app/components/ui/utils.ts b/VM1_app/figma/src/app/components/ui/utils.ts similarity index 100% rename from figma/src/app/components/ui/utils.ts rename to VM1_app/figma/src/app/components/ui/utils.ts diff --git a/figma/src/app/data/companyMessages.ts b/VM1_app/figma/src/app/data/companyMessages.ts similarity index 100% rename from figma/src/app/data/companyMessages.ts rename to VM1_app/figma/src/app/data/companyMessages.ts diff --git a/figma/src/app/data/session.ts b/VM1_app/figma/src/app/data/session.ts similarity index 100% rename from figma/src/app/data/session.ts rename to VM1_app/figma/src/app/data/session.ts diff --git a/figma/src/app/pages/AuthConsent.tsx b/VM1_app/figma/src/app/pages/AuthConsent.tsx similarity index 100% rename from figma/src/app/pages/AuthConsent.tsx rename to VM1_app/figma/src/app/pages/AuthConsent.tsx diff --git a/figma/src/app/pages/AuthLoading.tsx b/VM1_app/figma/src/app/pages/AuthLoading.tsx similarity index 100% rename from figma/src/app/pages/AuthLoading.tsx rename to VM1_app/figma/src/app/pages/AuthLoading.tsx diff --git a/figma/src/app/pages/AuthOtp.tsx b/VM1_app/figma/src/app/pages/AuthOtp.tsx similarity index 100% rename from figma/src/app/pages/AuthOtp.tsx rename to VM1_app/figma/src/app/pages/AuthOtp.tsx diff --git a/figma/src/app/pages/AuthPhone.tsx b/VM1_app/figma/src/app/pages/AuthPhone.tsx similarity index 100% rename from figma/src/app/pages/AuthPhone.tsx rename to VM1_app/figma/src/app/pages/AuthPhone.tsx diff --git a/figma/src/app/pages/Calendar.tsx b/VM1_app/figma/src/app/pages/Calendar.tsx similarity index 100% rename from figma/src/app/pages/Calendar.tsx rename to VM1_app/figma/src/app/pages/Calendar.tsx diff --git a/figma/src/app/pages/Chat.tsx b/VM1_app/figma/src/app/pages/Chat.tsx similarity index 100% rename from figma/src/app/pages/Chat.tsx rename to VM1_app/figma/src/app/pages/Chat.tsx diff --git a/figma/src/app/pages/History.tsx b/VM1_app/figma/src/app/pages/History.tsx similarity index 100% rename from figma/src/app/pages/History.tsx rename to VM1_app/figma/src/app/pages/History.tsx diff --git a/figma/src/app/pages/Home.tsx b/VM1_app/figma/src/app/pages/Home.tsx similarity index 100% rename from figma/src/app/pages/Home.tsx rename to VM1_app/figma/src/app/pages/Home.tsx diff --git a/figma/src/app/pages/NotificationDetail.tsx b/VM1_app/figma/src/app/pages/NotificationDetail.tsx similarity index 100% rename from figma/src/app/pages/NotificationDetail.tsx rename to VM1_app/figma/src/app/pages/NotificationDetail.tsx diff --git a/figma/src/app/pages/Profile.tsx b/VM1_app/figma/src/app/pages/Profile.tsx similarity index 100% rename from figma/src/app/pages/Profile.tsx rename to VM1_app/figma/src/app/pages/Profile.tsx diff --git a/figma/src/app/pages/Root.tsx b/VM1_app/figma/src/app/pages/Root.tsx similarity index 100% rename from figma/src/app/pages/Root.tsx rename to VM1_app/figma/src/app/pages/Root.tsx diff --git a/figma/src/app/routes.tsx b/VM1_app/figma/src/app/routes.tsx similarity index 100% rename from figma/src/app/routes.tsx rename to VM1_app/figma/src/app/routes.tsx diff --git a/figma/src/main.tsx b/VM1_app/figma/src/main.tsx similarity index 100% rename from figma/src/main.tsx rename to VM1_app/figma/src/main.tsx diff --git a/figma/src/styles/fonts.css b/VM1_app/figma/src/styles/fonts.css similarity index 100% rename from figma/src/styles/fonts.css rename to VM1_app/figma/src/styles/fonts.css diff --git a/figma/src/styles/globals.css b/VM1_app/figma/src/styles/globals.css similarity index 100% rename from figma/src/styles/globals.css rename to VM1_app/figma/src/styles/globals.css diff --git a/figma/src/styles/index.css b/VM1_app/figma/src/styles/index.css similarity index 100% rename from figma/src/styles/index.css rename to VM1_app/figma/src/styles/index.css diff --git a/figma/src/styles/tailwind.css b/VM1_app/figma/src/styles/tailwind.css similarity index 100% rename from figma/src/styles/tailwind.css rename to VM1_app/figma/src/styles/tailwind.css diff --git a/figma/src/styles/theme.css b/VM1_app/figma/src/styles/theme.css similarity index 100% rename from figma/src/styles/theme.css rename to VM1_app/figma/src/styles/theme.css diff --git a/figma/vite.config.ts b/VM1_app/figma/vite.config.ts similarity index 100% rename from figma/vite.config.ts rename to VM1_app/figma/vite.config.ts diff --git a/VM2_services/codebase/.gitignore b/VM2_services/codebase/.gitignore new file mode 100644 index 0000000..421904f --- /dev/null +++ b/VM2_services/codebase/.gitignore @@ -0,0 +1,33 @@ +# Local deployment configuration and secrets +backend/.env +backend/secrets/ +*.pem +*.key +*.p12 + +# Python +__pycache__/ +*.py[cod] +.pytest_cache/ +.mypy_cache/ +.ruff_cache/ +.venv/ +htmlcov/ +.coverage + +# JavaScript / Expo / Playwright +node_modules/ +.expo/ +dist/ +web-build/ +playwright-report/ +test-results/ + +# Java +target/ + +# IDE and OS +.idea/ +.vscode/ +.DS_Store +Thumbs.db diff --git a/codebase/services/.env.example b/VM2_services/codebase/services/.env.example similarity index 100% rename from codebase/services/.env.example rename to VM2_services/codebase/services/.env.example diff --git a/codebase/services/.gitignore b/VM2_services/codebase/services/.gitignore similarity index 100% rename from codebase/services/.gitignore rename to VM2_services/codebase/services/.gitignore diff --git a/codebase/services/bitrix-sync/.env.example b/VM2_services/codebase/services/bitrix-sync/.env.example similarity index 100% rename from codebase/services/bitrix-sync/.env.example rename to VM2_services/codebase/services/bitrix-sync/.env.example diff --git a/codebase/services/bitrix-sync/Dockerfile b/VM2_services/codebase/services/bitrix-sync/Dockerfile similarity index 100% rename from codebase/services/bitrix-sync/Dockerfile rename to VM2_services/codebase/services/bitrix-sync/Dockerfile diff --git a/codebase/services/bitrix-sync/README.md b/VM2_services/codebase/services/bitrix-sync/README.md similarity index 100% rename from codebase/services/bitrix-sync/README.md rename to VM2_services/codebase/services/bitrix-sync/README.md diff --git a/codebase/services/bitrix-sync/alembic.ini b/VM2_services/codebase/services/bitrix-sync/alembic.ini similarity index 100% rename from codebase/services/bitrix-sync/alembic.ini rename to VM2_services/codebase/services/bitrix-sync/alembic.ini diff --git a/codebase/services/bitrix-sync/alembic/env.py b/VM2_services/codebase/services/bitrix-sync/alembic/env.py similarity index 100% rename from codebase/services/bitrix-sync/alembic/env.py rename to VM2_services/codebase/services/bitrix-sync/alembic/env.py diff --git a/codebase/services/bitrix-sync/alembic/versions/0000_legacy_sync_baseline.py b/VM2_services/codebase/services/bitrix-sync/alembic/versions/0000_legacy_sync_baseline.py similarity index 100% rename from codebase/services/bitrix-sync/alembic/versions/0000_legacy_sync_baseline.py rename to VM2_services/codebase/services/bitrix-sync/alembic/versions/0000_legacy_sync_baseline.py diff --git a/codebase/services/bitrix-sync/alembic/versions/0001_bitrix_sync_full.py b/VM2_services/codebase/services/bitrix-sync/alembic/versions/0001_bitrix_sync_full.py similarity index 100% rename from codebase/services/bitrix-sync/alembic/versions/0001_bitrix_sync_full.py rename to VM2_services/codebase/services/bitrix-sync/alembic/versions/0001_bitrix_sync_full.py diff --git a/codebase/services/bitrix-sync/alembic/versions/0002_app_queue_contract.py b/VM2_services/codebase/services/bitrix-sync/alembic/versions/0002_app_queue_contract.py similarity index 100% rename from codebase/services/bitrix-sync/alembic/versions/0002_app_queue_contract.py rename to VM2_services/codebase/services/bitrix-sync/alembic/versions/0002_app_queue_contract.py diff --git a/codebase/services/bitrix-sync/app/__init__.py b/VM2_services/codebase/services/bitrix-sync/app/__init__.py similarity index 100% rename from codebase/services/bitrix-sync/app/__init__.py rename to VM2_services/codebase/services/bitrix-sync/app/__init__.py diff --git a/codebase/services/bitrix-sync/app/config.py b/VM2_services/codebase/services/bitrix-sync/app/config.py similarity index 100% rename from codebase/services/bitrix-sync/app/config.py rename to VM2_services/codebase/services/bitrix-sync/app/config.py diff --git a/codebase/services/bitrix-sync/app/crm.py b/VM2_services/codebase/services/bitrix-sync/app/crm.py similarity index 100% rename from codebase/services/bitrix-sync/app/crm.py rename to VM2_services/codebase/services/bitrix-sync/app/crm.py diff --git a/codebase/services/bitrix-sync/app/domain.py b/VM2_services/codebase/services/bitrix-sync/app/domain.py similarity index 100% rename from codebase/services/bitrix-sync/app/domain.py rename to VM2_services/codebase/services/bitrix-sync/app/domain.py diff --git a/codebase/services/bitrix-sync/app/engine.py b/VM2_services/codebase/services/bitrix-sync/app/engine.py similarity index 100% rename from codebase/services/bitrix-sync/app/engine.py rename to VM2_services/codebase/services/bitrix-sync/app/engine.py diff --git a/codebase/services/bitrix-sync/app/main.py b/VM2_services/codebase/services/bitrix-sync/app/main.py similarity index 100% rename from codebase/services/bitrix-sync/app/main.py rename to VM2_services/codebase/services/bitrix-sync/app/main.py diff --git a/codebase/services/bitrix-sync/app/mapping.py b/VM2_services/codebase/services/bitrix-sync/app/mapping.py similarity index 100% rename from codebase/services/bitrix-sync/app/mapping.py rename to VM2_services/codebase/services/bitrix-sync/app/mapping.py diff --git a/codebase/services/bitrix-sync/app/reconciliation.py b/VM2_services/codebase/services/bitrix-sync/app/reconciliation.py similarity index 100% rename from codebase/services/bitrix-sync/app/reconciliation.py rename to VM2_services/codebase/services/bitrix-sync/app/reconciliation.py diff --git a/codebase/services/bitrix-sync/app/repository.py b/VM2_services/codebase/services/bitrix-sync/app/repository.py similarity index 100% rename from codebase/services/bitrix-sync/app/repository.py rename to VM2_services/codebase/services/bitrix-sync/app/repository.py diff --git a/codebase/services/bitrix-sync/app/security.py b/VM2_services/codebase/services/bitrix-sync/app/security.py similarity index 100% rename from codebase/services/bitrix-sync/app/security.py rename to VM2_services/codebase/services/bitrix-sync/app/security.py diff --git a/codebase/services/bitrix-sync/app/worker.py b/VM2_services/codebase/services/bitrix-sync/app/worker.py similarity index 100% rename from codebase/services/bitrix-sync/app/worker.py rename to VM2_services/codebase/services/bitrix-sync/app/worker.py diff --git a/codebase/services/bitrix-sync/compose.fragment.yaml b/VM2_services/codebase/services/bitrix-sync/compose.fragment.yaml similarity index 100% rename from codebase/services/bitrix-sync/compose.fragment.yaml rename to VM2_services/codebase/services/bitrix-sync/compose.fragment.yaml diff --git a/codebase/services/bitrix-sync/openapi.yaml b/VM2_services/codebase/services/bitrix-sync/openapi.yaml similarity index 100% rename from codebase/services/bitrix-sync/openapi.yaml rename to VM2_services/codebase/services/bitrix-sync/openapi.yaml diff --git a/codebase/services/bitrix-sync/pyproject.toml b/VM2_services/codebase/services/bitrix-sync/pyproject.toml similarity index 100% rename from codebase/services/bitrix-sync/pyproject.toml rename to VM2_services/codebase/services/bitrix-sync/pyproject.toml diff --git a/codebase/services/bitrix-sync/tests/conftest.py b/VM2_services/codebase/services/bitrix-sync/tests/conftest.py similarity index 100% rename from codebase/services/bitrix-sync/tests/conftest.py rename to VM2_services/codebase/services/bitrix-sync/tests/conftest.py diff --git a/codebase/services/bitrix-sync/tests/test_config.py b/VM2_services/codebase/services/bitrix-sync/tests/test_config.py similarity index 100% rename from codebase/services/bitrix-sync/tests/test_config.py rename to VM2_services/codebase/services/bitrix-sync/tests/test_config.py diff --git a/codebase/services/bitrix-sync/tests/test_crm.py b/VM2_services/codebase/services/bitrix-sync/tests/test_crm.py similarity index 100% rename from codebase/services/bitrix-sync/tests/test_crm.py rename to VM2_services/codebase/services/bitrix-sync/tests/test_crm.py diff --git a/codebase/services/bitrix-sync/tests/test_domain.py b/VM2_services/codebase/services/bitrix-sync/tests/test_domain.py similarity index 100% rename from codebase/services/bitrix-sync/tests/test_domain.py rename to VM2_services/codebase/services/bitrix-sync/tests/test_domain.py diff --git a/codebase/services/bitrix-sync/tests/test_engine_boundaries.py b/VM2_services/codebase/services/bitrix-sync/tests/test_engine_boundaries.py similarity index 100% rename from codebase/services/bitrix-sync/tests/test_engine_boundaries.py rename to VM2_services/codebase/services/bitrix-sync/tests/test_engine_boundaries.py diff --git a/codebase/services/bitrix-sync/tests/test_webhook_security.py b/VM2_services/codebase/services/bitrix-sync/tests/test_webhook_security.py similarity index 100% rename from codebase/services/bitrix-sync/tests/test_webhook_security.py rename to VM2_services/codebase/services/bitrix-sync/tests/test_webhook_security.py diff --git a/codebase/services/deployment/RUNBOOK.md b/VM2_services/codebase/services/deployment/RUNBOOK.md similarity index 100% rename from codebase/services/deployment/RUNBOOK.md rename to VM2_services/codebase/services/deployment/RUNBOOK.md diff --git a/codebase/services/deployment/RUNBOOK.ru.md b/VM2_services/codebase/services/deployment/RUNBOOK.ru.md similarity index 100% rename from codebase/services/deployment/RUNBOOK.ru.md rename to VM2_services/codebase/services/deployment/RUNBOOK.ru.md diff --git a/codebase/services/deployment/deploy-message-safety-mode.sudoers b/VM2_services/codebase/services/deployment/deploy-message-safety-mode.sudoers similarity index 100% rename from codebase/services/deployment/deploy-message-safety-mode.sudoers rename to VM2_services/codebase/services/deployment/deploy-message-safety-mode.sudoers diff --git a/codebase/services/deployment/han-message-safety-mode b/VM2_services/codebase/services/deployment/han-message-safety-mode similarity index 100% rename from codebase/services/deployment/han-message-safety-mode rename to VM2_services/codebase/services/deployment/han-message-safety-mode diff --git a/codebase/services/deployment/han-processing.service b/VM2_services/codebase/services/deployment/han-processing.service similarity index 100% rename from codebase/services/deployment/han-processing.service rename to VM2_services/codebase/services/deployment/han-processing.service diff --git a/codebase/services/deployment/preflight.sh b/VM2_services/codebase/services/deployment/preflight.sh similarity index 100% rename from codebase/services/deployment/preflight.sh rename to VM2_services/codebase/services/deployment/preflight.sh diff --git a/codebase/services/deployment/scripts/setup-vm.sh b/VM2_services/codebase/services/deployment/scripts/setup-vm.sh similarity index 100% rename from codebase/services/deployment/scripts/setup-vm.sh rename to VM2_services/codebase/services/deployment/scripts/setup-vm.sh diff --git a/codebase/services/deployment/scripts/ssl-renew-deploy-hook.sh b/VM2_services/codebase/services/deployment/scripts/ssl-renew-deploy-hook.sh similarity index 100% rename from codebase/services/deployment/scripts/ssl-renew-deploy-hook.sh rename to VM2_services/codebase/services/deployment/scripts/ssl-renew-deploy-hook.sh diff --git a/codebase/services/deployment/secrets/config.example.json b/VM2_services/codebase/services/deployment/secrets/config.example.json similarity index 100% rename from codebase/services/deployment/secrets/config.example.json rename to VM2_services/codebase/services/deployment/secrets/config.example.json diff --git a/codebase/services/deployment/secrets/han-compose b/VM2_services/codebase/services/deployment/secrets/han-compose similarity index 100% rename from codebase/services/deployment/secrets/han-compose rename to VM2_services/codebase/services/deployment/secrets/han-compose diff --git a/codebase/services/deployment/secrets/han-secrets b/VM2_services/codebase/services/deployment/secrets/han-secrets similarity index 100% rename from codebase/services/deployment/secrets/han-secrets rename to VM2_services/codebase/services/deployment/secrets/han-secrets diff --git a/codebase/services/deployment/secrets/han-secrets-vm2.service b/VM2_services/codebase/services/deployment/secrets/han-secrets-vm2.service similarity index 100% rename from codebase/services/deployment/secrets/han-secrets-vm2.service rename to VM2_services/codebase/services/deployment/secrets/han-secrets-vm2.service diff --git a/codebase/services/deployment/secrets/secrets_loader.py b/VM2_services/codebase/services/deployment/secrets/secrets_loader.py similarity index 100% rename from codebase/services/deployment/secrets/secrets_loader.py rename to VM2_services/codebase/services/deployment/secrets/secrets_loader.py diff --git a/codebase/services/docker-compose.yml b/VM2_services/codebase/services/docker-compose.yml similarity index 100% rename from codebase/services/docker-compose.yml rename to VM2_services/codebase/services/docker-compose.yml diff --git a/codebase/services/message-safety/Dockerfile b/VM2_services/codebase/services/message-safety/Dockerfile similarity index 100% rename from codebase/services/message-safety/Dockerfile rename to VM2_services/codebase/services/message-safety/Dockerfile diff --git a/codebase/services/message-safety/README.md b/VM2_services/codebase/services/message-safety/README.md similarity index 100% rename from codebase/services/message-safety/README.md rename to VM2_services/codebase/services/message-safety/README.md diff --git a/codebase/services/message-safety/alembic.ini b/VM2_services/codebase/services/message-safety/alembic.ini similarity index 100% rename from codebase/services/message-safety/alembic.ini rename to VM2_services/codebase/services/message-safety/alembic.ini diff --git a/codebase/services/message-safety/alembic/env.py b/VM2_services/codebase/services/message-safety/alembic/env.py similarity index 100% rename from codebase/services/message-safety/alembic/env.py rename to VM2_services/codebase/services/message-safety/alembic/env.py diff --git a/codebase/services/message-safety/alembic/versions/0001_message_safety_v2.py b/VM2_services/codebase/services/message-safety/alembic/versions/0001_message_safety_v2.py similarity index 100% rename from codebase/services/message-safety/alembic/versions/0001_message_safety_v2.py rename to VM2_services/codebase/services/message-safety/alembic/versions/0001_message_safety_v2.py diff --git a/codebase/services/message-safety/app/__init__.py b/VM2_services/codebase/services/message-safety/app/__init__.py similarity index 100% rename from codebase/services/message-safety/app/__init__.py rename to VM2_services/codebase/services/message-safety/app/__init__.py diff --git a/codebase/services/message-safety/app/adapters.py b/VM2_services/codebase/services/message-safety/app/adapters.py similarity index 100% rename from codebase/services/message-safety/app/adapters.py rename to VM2_services/codebase/services/message-safety/app/adapters.py diff --git a/codebase/services/message-safety/app/api.py b/VM2_services/codebase/services/message-safety/app/api.py similarity index 100% rename from codebase/services/message-safety/app/api.py rename to VM2_services/codebase/services/message-safety/app/api.py diff --git a/codebase/services/message-safety/app/artifacts/config.schema.json b/VM2_services/codebase/services/message-safety/app/artifacts/config.schema.json similarity index 100% rename from codebase/services/message-safety/app/artifacts/config.schema.json rename to VM2_services/codebase/services/message-safety/app/artifacts/config.schema.json diff --git a/codebase/services/message-safety/app/artifacts/detector-manifest.json b/VM2_services/codebase/services/message-safety/app/artifacts/detector-manifest.json similarity index 100% rename from codebase/services/message-safety/app/artifacts/detector-manifest.json rename to VM2_services/codebase/services/message-safety/app/artifacts/detector-manifest.json diff --git a/codebase/services/message-safety/app/artifacts/rules/rules-2026-01-01/rules.yaml b/VM2_services/codebase/services/message-safety/app/artifacts/rules/rules-2026-01-01/rules.yaml similarity index 100% rename from codebase/services/message-safety/app/artifacts/rules/rules-2026-01-01/rules.yaml rename to VM2_services/codebase/services/message-safety/app/artifacts/rules/rules-2026-01-01/rules.yaml diff --git a/codebase/services/message-safety/app/artifacts/rules/rules.schema.json b/VM2_services/codebase/services/message-safety/app/artifacts/rules/rules.schema.json similarity index 100% rename from codebase/services/message-safety/app/artifacts/rules/rules.schema.json rename to VM2_services/codebase/services/message-safety/app/artifacts/rules/rules.schema.json diff --git a/codebase/services/message-safety/app/artifacts/seed-config.yaml b/VM2_services/codebase/services/message-safety/app/artifacts/seed-config.yaml similarity index 100% rename from codebase/services/message-safety/app/artifacts/seed-config.yaml rename to VM2_services/codebase/services/message-safety/app/artifacts/seed-config.yaml diff --git a/codebase/services/message-safety/app/config.py b/VM2_services/codebase/services/message-safety/app/config.py similarity index 100% rename from codebase/services/message-safety/app/config.py rename to VM2_services/codebase/services/message-safety/app/config.py diff --git a/codebase/services/message-safety/app/config_admin.py b/VM2_services/codebase/services/message-safety/app/config_admin.py similarity index 100% rename from codebase/services/message-safety/app/config_admin.py rename to VM2_services/codebase/services/message-safety/app/config_admin.py diff --git a/codebase/services/message-safety/app/contracts.py b/VM2_services/codebase/services/message-safety/app/contracts.py similarity index 100% rename from codebase/services/message-safety/app/contracts.py rename to VM2_services/codebase/services/message-safety/app/contracts.py diff --git a/codebase/services/message-safety/app/db.py b/VM2_services/codebase/services/message-safety/app/db.py similarity index 100% rename from codebase/services/message-safety/app/db.py rename to VM2_services/codebase/services/message-safety/app/db.py diff --git a/codebase/services/message-safety/app/file_pipeline.py b/VM2_services/codebase/services/message-safety/app/file_pipeline.py similarity index 100% rename from codebase/services/message-safety/app/file_pipeline.py rename to VM2_services/codebase/services/message-safety/app/file_pipeline.py diff --git a/codebase/services/message-safety/app/fingerprint.py b/VM2_services/codebase/services/message-safety/app/fingerprint.py similarity index 100% rename from codebase/services/message-safety/app/fingerprint.py rename to VM2_services/codebase/services/message-safety/app/fingerprint.py diff --git a/codebase/services/message-safety/app/hot_cache.py b/VM2_services/codebase/services/message-safety/app/hot_cache.py similarity index 100% rename from codebase/services/message-safety/app/hot_cache.py rename to VM2_services/codebase/services/message-safety/app/hot_cache.py diff --git a/codebase/services/message-safety/app/main.py b/VM2_services/codebase/services/message-safety/app/main.py similarity index 100% rename from codebase/services/message-safety/app/main.py rename to VM2_services/codebase/services/message-safety/app/main.py diff --git a/codebase/services/message-safety/app/normalization.py b/VM2_services/codebase/services/message-safety/app/normalization.py similarity index 100% rename from codebase/services/message-safety/app/normalization.py rename to VM2_services/codebase/services/message-safety/app/normalization.py diff --git a/codebase/services/message-safety/app/rate_limit.py b/VM2_services/codebase/services/message-safety/app/rate_limit.py similarity index 100% rename from codebase/services/message-safety/app/rate_limit.py rename to VM2_services/codebase/services/message-safety/app/rate_limit.py diff --git a/codebase/services/message-safety/app/repository.py b/VM2_services/codebase/services/message-safety/app/repository.py similarity index 100% rename from codebase/services/message-safety/app/repository.py rename to VM2_services/codebase/services/message-safety/app/repository.py diff --git a/codebase/services/message-safety/app/rules.py b/VM2_services/codebase/services/message-safety/app/rules.py similarity index 100% rename from codebase/services/message-safety/app/rules.py rename to VM2_services/codebase/services/message-safety/app/rules.py diff --git a/codebase/services/message-safety/app/service.py b/VM2_services/codebase/services/message-safety/app/service.py similarity index 100% rename from codebase/services/message-safety/app/service.py rename to VM2_services/codebase/services/message-safety/app/service.py diff --git a/codebase/services/message-safety/app/settings.py b/VM2_services/codebase/services/message-safety/app/settings.py similarity index 100% rename from codebase/services/message-safety/app/settings.py rename to VM2_services/codebase/services/message-safety/app/settings.py diff --git a/codebase/services/message-safety/app/url_policy.py b/VM2_services/codebase/services/message-safety/app/url_policy.py similarity index 100% rename from codebase/services/message-safety/app/url_policy.py rename to VM2_services/codebase/services/message-safety/app/url_policy.py diff --git a/codebase/services/message-safety/app/worker.py b/VM2_services/codebase/services/message-safety/app/worker.py similarity index 100% rename from codebase/services/message-safety/app/worker.py rename to VM2_services/codebase/services/message-safety/app/worker.py diff --git a/codebase/services/message-safety/docker-compose.fragment.yml b/VM2_services/codebase/services/message-safety/docker-compose.fragment.yml similarity index 100% rename from codebase/services/message-safety/docker-compose.fragment.yml rename to VM2_services/codebase/services/message-safety/docker-compose.fragment.yml diff --git a/codebase/services/message-safety/entrypoint.sh b/VM2_services/codebase/services/message-safety/entrypoint.sh similarity index 100% rename from codebase/services/message-safety/entrypoint.sh rename to VM2_services/codebase/services/message-safety/entrypoint.sh diff --git a/codebase/services/message-safety/openapi.yaml b/VM2_services/codebase/services/message-safety/openapi.yaml similarity index 100% rename from codebase/services/message-safety/openapi.yaml rename to VM2_services/codebase/services/message-safety/openapi.yaml diff --git a/codebase/services/message-safety/pyproject.toml b/VM2_services/codebase/services/message-safety/pyproject.toml similarity index 100% rename from codebase/services/message-safety/pyproject.toml rename to VM2_services/codebase/services/message-safety/pyproject.toml diff --git a/codebase/services/message-safety/tests/conftest.py b/VM2_services/codebase/services/message-safety/tests/conftest.py similarity index 100% rename from codebase/services/message-safety/tests/conftest.py rename to VM2_services/codebase/services/message-safety/tests/conftest.py diff --git a/codebase/services/message-safety/tests/test_api_contract.py b/VM2_services/codebase/services/message-safety/tests/test_api_contract.py similarity index 100% rename from codebase/services/message-safety/tests/test_api_contract.py rename to VM2_services/codebase/services/message-safety/tests/test_api_contract.py diff --git a/codebase/services/message-safety/tests/test_config_and_schema.py b/VM2_services/codebase/services/message-safety/tests/test_config_and_schema.py similarity index 100% rename from codebase/services/message-safety/tests/test_config_and_schema.py rename to VM2_services/codebase/services/message-safety/tests/test_config_and_schema.py diff --git a/codebase/services/message-safety/tests/test_determinism.py b/VM2_services/codebase/services/message-safety/tests/test_determinism.py similarity index 100% rename from codebase/services/message-safety/tests/test_determinism.py rename to VM2_services/codebase/services/message-safety/tests/test_determinism.py diff --git a/codebase/services/message-safety/tests/test_files.py b/VM2_services/codebase/services/message-safety/tests/test_files.py similarity index 100% rename from codebase/services/message-safety/tests/test_files.py rename to VM2_services/codebase/services/message-safety/tests/test_files.py diff --git a/codebase/services/message-safety/tests/test_openapi.py b/VM2_services/codebase/services/message-safety/tests/test_openapi.py similarity index 100% rename from codebase/services/message-safety/tests/test_openapi.py rename to VM2_services/codebase/services/message-safety/tests/test_openapi.py diff --git a/codebase/services/message-safety/tests/test_rules_and_urls.py b/VM2_services/codebase/services/message-safety/tests/test_rules_and_urls.py similarity index 100% rename from codebase/services/message-safety/tests/test_rules_and_urls.py rename to VM2_services/codebase/services/message-safety/tests/test_rules_and_urls.py diff --git a/codebase/services/nginx/allowlists/bitrix-webhook-allowlist.conf b/VM2_services/codebase/services/nginx/allowlists/bitrix-webhook-allowlist.conf similarity index 100% rename from codebase/services/nginx/allowlists/bitrix-webhook-allowlist.conf rename to VM2_services/codebase/services/nginx/allowlists/bitrix-webhook-allowlist.conf diff --git a/codebase/services/nginx/allowlists/bitrix-webhook-allowlist.conf.template b/VM2_services/codebase/services/nginx/allowlists/bitrix-webhook-allowlist.conf.template similarity index 100% rename from codebase/services/nginx/allowlists/bitrix-webhook-allowlist.conf.template rename to VM2_services/codebase/services/nginx/allowlists/bitrix-webhook-allowlist.conf.template diff --git a/codebase/services/nginx/allowlists/private-caller-allowlist.conf b/VM2_services/codebase/services/nginx/allowlists/private-caller-allowlist.conf similarity index 100% rename from codebase/services/nginx/allowlists/private-caller-allowlist.conf rename to VM2_services/codebase/services/nginx/allowlists/private-caller-allowlist.conf diff --git a/codebase/services/nginx/allowlists/private-caller-allowlist.conf.template b/VM2_services/codebase/services/nginx/allowlists/private-caller-allowlist.conf.template similarity index 100% rename from codebase/services/nginx/allowlists/private-caller-allowlist.conf.template rename to VM2_services/codebase/services/nginx/allowlists/private-caller-allowlist.conf.template diff --git a/codebase/services/nginx/allowlists/proxy-common.conf b/VM2_services/codebase/services/nginx/allowlists/proxy-common.conf similarity index 100% rename from codebase/services/nginx/allowlists/proxy-common.conf rename to VM2_services/codebase/services/nginx/allowlists/proxy-common.conf diff --git a/codebase/services/nginx/allowlists/tls.conf b/VM2_services/codebase/services/nginx/allowlists/tls.conf similarity index 100% rename from codebase/services/nginx/allowlists/tls.conf rename to VM2_services/codebase/services/nginx/allowlists/tls.conf diff --git a/codebase/services/nginx/nginx.conf b/VM2_services/codebase/services/nginx/nginx.conf similarity index 100% rename from codebase/services/nginx/nginx.conf rename to VM2_services/codebase/services/nginx/nginx.conf diff --git a/codebase/services/nginx/templates/10-vm2.conf.template b/VM2_services/codebase/services/nginx/templates/10-vm2.conf.template similarity index 100% rename from codebase/services/nginx/templates/10-vm2.conf.template rename to VM2_services/codebase/services/nginx/templates/10-vm2.conf.template diff --git a/codebase/services/observability/otel-collector.yaml b/VM2_services/codebase/services/observability/otel-collector.yaml similarity index 100% rename from codebase/services/observability/otel-collector.yaml rename to VM2_services/codebase/services/observability/otel-collector.yaml diff --git a/codebase/services/redis/redis-safety.acl.template b/VM2_services/codebase/services/redis/redis-safety.acl.template similarity index 100% rename from codebase/services/redis/redis-safety.acl.template rename to VM2_services/codebase/services/redis/redis-safety.acl.template diff --git a/codebase/services/redis/redis.conf b/VM2_services/codebase/services/redis/redis.conf similarity index 100% rename from codebase/services/redis/redis.conf rename to VM2_services/codebase/services/redis/redis.conf diff --git a/VM2_services/documentation/README.md b/VM2_services/documentation/README.md new file mode 100644 index 0000000..e3745f5 --- /dev/null +++ b/VM2_services/documentation/README.md @@ -0,0 +1,14 @@ +# Документация ВМ2 Processing + +Здесь находятся профильные спецификации контура ВМ2. Канонические границы, имена и межсервисные контракты задаёт [`architectory`](../../architectory/README.md); при конфликте действует порядок приоритетов из этого README. + +## Состав ВМ2 + +- [`module-03-nginx-vm2.md`](module-03-nginx-vm2.md) — отдельный public webhook ingress и private `:8443`. +- [`module-04-redis-vm2.md`](module-04-redis-vm2.md) — Redis Safety. +- [`module-05-message-safety.md`](module-05-message-safety.md) — Message Safety v2. +- [`module-07-bitrix-sync.md`](module-07-bitrix-sync.md) — CRM sync и exact webhook. +- [`module-09-observability-vm2.md`](module-09-observability-vm2.md) — telemetry ВМ2. +- [`module-10-deployment-vm2.md`](module-10-deployment-vm2.md) — runbook ВМ2. + +Guest API, frontend, Keycloak, SMS и Bitrix Open Lines local app находятся в [`VM1_app/documentation`](../../VM1_app/documentation/README.md). ВМ2 публикует на `443` только два exact CRM webhook; Safety доступен ВМ1 только через private HTTPS `:8443`. diff --git a/VM2_services/documentation/module-03-nginx-vm2.md b/VM2_services/documentation/module-03-nginx-vm2.md new file mode 100644 index 0000000..6921c1f --- /dev/null +++ b/VM2_services/documentation/module-03-nginx-vm2.md @@ -0,0 +1,145 @@ +# module-03-vm2. Nginx ВМ2 Processing + +> Статус: целевая спецификация nginx на ВМ2. +> Канонический контракт (TLS/ACME, request id, internal 404, logs, reload) — [`arch-08-nginx.md`](../../architectory/arch-08-nginx.md). +> Контур ВМ1 — [`module-03-nginx-vm1.md`](../../VM1_app/documentation/module-03-nginx-vm1.md). Guest API, SPA, auth и SMS callback на этой машине не публикуются. + +## 1. Назначение и границы + +Nginx ВМ2 — самостоятельный ingress Processing: + +- public `80/443` на отдельном DNS host: ACME/redirect и два exact CRM webhook; +- private `8443` с сертификатом internal CA: только server-to-server Message Safety и approved ops. + +Публичный трафик ВМ2 не проходит через ВМ1. Отказ nginx ВМ1 не прерывает приём CRM webhook. SPA, `/api/`, `/auth/`, WS и Direct SMS callback здесь отсутствуют. + +## 2. Routing matrix + +### Public `80/443` + +| Внешний путь | Upstream | Режим | +|---|---|---| +| exact `/bitrix/sync/webhook/contact` | `bitrix-sync:8080` | public HTTPS Contact event; source IP CIDR/method/body/rate limits, query-token auth в upstream | +| exact `/bitrix/sync/webhook/alert` | `bitrix-sync:8080` | public HTTPS smart-process event; те же ограничения | + +До full sync cutover оба exact webhook route закрыты либо возвращают retryable `503`; успешный `2xx ignored` запрещён. + +Query не участвует в exact location matching: URL штатного робота `/bitrix/sync/webhook/?token=...&ID=...` попадает в соответствующий exact route. + +`/internal/`, `/_internal/`, Redis/OTLP/admin/status/config files на public host → `404`. Fallback на ВМ1 или SPA запрещён. Прочие paths/methods → `404/405`. + +### Private `8443` + +| Path | Local upstream | Caller | +|---|---|---| +| `/internal/safety/v2/*` | `message-safety-api:8080` | api-backend ВМ1 | +| exact `/internal/safety/status` | `message-safety-api:8080/health/ready` | api-backend ВМ1, ops; capability snapshot, не correctness gate | +| `/internal/sync/v1/*` | `bitrix-sync:8080` | ops/allow-listed service | + +Public и private server blocks не имеют общего fallback. Safety status alias доступен только на private listener; public `/health/ready` и `/internal/safety/status` не публикуются. + +## 3. Webhook allow-list и preflight + +До proxy nginx проверяет непосредственный source IP по version-controlled `BITRIX_WEBHOOK_ALLOWED_CIDRS`; пустой/невалидный список при enabled receiver блокирует deployment. Адрес из недоверенного `X-Forwarded-For` не используется. + +`BITRIX_SYNC_ENABLED`, public route, readiness и allow-list согласуются одним +preflight: disabled требует `deny all;`, enabled — reviewed non-empty CIDR и +ready receiver. Обратные комбинации блокируют deployment. + +Запрос вне allow-list получает generic `403` без proxy. В безопасном журнале с ограниченным retention сохраняются только timestamp, source IP, route class и outcome; query/body не сохраняются. Telemetry pipeline экспортирует `webhook_rejected_total{receiver,reason="source_ip"}` без IP label. Allow-list не расширяется автоматически: всплеск Contact, восстановленных инкрементальной reconciliation, инициирует проверку rejected-IP журнала, подтверждение принадлежности адреса Битрикс24 и reviewed reload конфигурации. + +Bitrix webhook retries имеют отдельный достаточный burst, проходят source IP allow-list и проверяют query receiver token в сервисе. + +## 4. Private `8443` fail-closed + +До утверждённого Safety cutover active caller allow-list содержит только `deny all;`; после cutover он совпадает с SG/host-firewall источниками ВМ1. Расхождение любого из трёх слоёв (nginx allow-list, SG, host firewall) блокирует rollout. + +Private listener доверяет forwarded headers только от allow-listed private caller. + +## 5. Listeners и TLS + +- `:80` — только ACME challenge и `308 https://$host$request_uri`; +- исключение: `/bitrix/sync/webhook/contact|alert` на HTTP возвращают generic `404/426` **без redirect и отражения query token**; +- `:443` — только ACME-совместимый TLS и exact CRM webhook; +- private `:8443` — internal CA, SAN совпадает с private DNS именем ВМ2; +- public и private сертификаты разные; public key недоступен приложению. + +## 6. Timeouts и body limits ВМ2 + +CRM webhook: connect/send/read `3s / 30s / 60s` (arch-08). Private Safety не короче caller budget. JSON body webhook ограничен; file bytes через этот nginx не идут. Cache на webhook выключен. + +## 7. Edge rate limits ВМ2 + +Отдельные зоны для двух exact webhook (мягкий burst под retries Битрикс24) и `limit_conn`. Зоны guest API/auth/WS/SPA/SMS на ВМ2 не создаются. + +Ответ превышения — `429`, `Retry-After`, request id. Generic `403` для wrong source IP — до proxy, не `429`. + +## 8. Security headers + +Public webhook: не кэшировать, не ослаблять CSP ради SPA (SPA нет). `Server` / `X-Powered-By` скрыты (arch-08). CORS wildcard с credentials запрещён; webhook не требует browser CORS. + +## 9. Health и synthetic ВМ2 + +Внутренний `/nginx-health/live` — arch-08 §8. Synthetic отдельно: HTTP redirect/ACME policy, internal 404 на public host, GET webhook `405/404`, valid отдельный cert, POST webhook allowed/denied source IP без помещения query token в shell history или логи. Private `8443` проверяется с VM1/ops host, не из интернета. + +## 10. Layout и Compose ВМ2 + +Каркас arch-08 §10 плюс: + +- отдельный template/server block private `8443`; +- snippet/allow-list `BITRIX_WEBHOOK_ALLOWED_CIDRS`; +- без `websocket.conf` и без `frontend-static`. + +Public ports: `${NGINX_HTTP_PORT}:80`, `${NGINX_HTTPS_PORT}:443`. Private `8443` публикуется только в сеть/SG, разрешённую ВМ1 и ops — не как internet listener. Детали портов/SG — arch-03 / arch-06 / [`arch-10-deployment.md`](../../architectory/arch-10-deployment.md). + +## 11. Failure behavior ВМ2 + +Дополнительно к arch-08 §12: + +- rollback sync: закрыть public webhook routes либо вернуть retryable `503`; возврат к фиктивному `202 ignored` запрещён; +- ВМ1 при этом не изменяется; +- Safety slow на private `8443`: не маскировать ошибку `2xx`; caller ВМ1 держит poll budget. + +## 12. Валидация и тесты ВМ2 + +Автоматические тесты: + +- CRM webhook exact routes принимают query без изменения location matching; +- allowed source IP проксируется, wrong IP получает `403` до upstream; +- HTTP webhook URL с query token не перенаправляется на HTTPS и не отражает query в `Location`/error; +- source-IP rejects попадают в безопасный bounded-retention журнал и low-cardinality telemetry без query/body/IP label; +- disabled/`deny all` и enabled/ready/CIDR preflight: обратные комбинации блокируют deployment; +- public `/internal/safety/*` и `/internal/sync/*` → `404`; +- private exact `/internal/safety/status` возвращает capability readiness Safety, а соседние неразрешённые health paths → `404`; +- private `8443` до cutover `deny all`, после cutover только источники ВМ1; +- GET webhook `405/404`; прочие paths `404`; +- TLS scan public host и отдельная проверка internal CA на `8443`; +- ACME/redirect rehearsal без остановки приёма (кроме контролируемого окна); +- logs/traces не содержат query token, form body, Authorization. + +Acceptance не использует public host ВМ1 и не требует running nginx ВМ1. + +## 13. Definition of Done ВМ2 + +Дополнительно к arch-08 §13: + +- независимые public `80/443` и private `8443`; +- routing matrix §2 покрыта; +- CRM webhook достигает ВМ2 напрямую и продолжает приниматься при остановленном nginx ВМ1; +- CRM webhook ограничен version-controlled source IP CIDR allow-list; query token и form body отсутствуют в access/error logs и traces; +- preflight `BITRIX_SYNC_ENABLED` согласован с route/allow-list/readiness; +- private Safety не опубликован на public host. + +## 14. TBD ВМ2 + +- N1: доверенные WAF/LB CIDR перед public host ВМ2, если появятся; до этого allow-list — непосредственный source IP. +- N3: нужен ли публичный health на processing host. +- N6: финальный burst webhook зон. + +## 15. Ссылки + +- Контракт: [`arch-08-nginx.md`](../../architectory/arch-08-nginx.md). +- ВМ1: [`module-03-nginx-vm1.md`](../../VM1_app/documentation/module-03-nginx-vm1.md). +- Указатель: [`module-03-nginx.md`](module-03-nginx.md). +- Safety / sync / observability: [`module-05-message-safety.md`](module-05-message-safety.md), [`module-07-bitrix-sync.md`](module-07-bitrix-sync.md), [`module-09-observability-vm2.md`](module-09-observability-vm2.md). +- Деплой: [`module-10-deployment-vm2.md`](module-10-deployment-vm2.md). diff --git a/VM2_services/documentation/module-04-redis-vm2.md b/VM2_services/documentation/module-04-redis-vm2.md new file mode 100644 index 0000000..722f7c4 --- /dev/null +++ b/VM2_services/documentation/module-04-redis-vm2.md @@ -0,0 +1,107 @@ +# module-04-vm2. Redis Safety ВМ2 Processing + +> Статус: целевая спецификация Redis на ВМ2. +> Канонический контракт (ключи, TTL, Lua, AOF, ACL, eviction) — [`arch-09-redis.md`](../../architectory/arch-09-redis.md). +> Redis DB0/DB1 ВМ1 — [`module-04-redis-vm1.md`](../../VM1_app/documentation/module-04-redis-vm1.md). Hostname Redis ВМ1 не используется. + +## 1. Назначение и границы + +Отдельный instance Redis Safety: hot cache, rate limiting caller и optional worker wake-up. PostgreSQL `message_safety.safety_tasks` — единственный queue/lease source (`FOR UPDATE SKIP LOCKED`, fencing generation). Redis не хранит authoritative task state, locks или leases. + +`bitrix-sync` этот Redis не использует. Guest API rate/idempotency/realtime — на ВМ1. + +Cache loss/restart безопасно восстанавливается из PostgreSQL; Redis outage не выключает core Safety. + +## 2. URL и ACL + +```text +MESSAGE_SAFETY_REDIS_URL=redis://message_safety:@redis:6379/0 +``` + +Только на ВМ2. Credential доставляется secret file и не входит в общий `.env` ВМ1. `message_safety` ACL: только prefixes `han:safety:*`. `REDIS_URL` / `REDIS_REALTIME_URL` на ВМ2 отсутствуют. + +Logical DB index `/0` — локальный этой instance, не DB0 ВМ1. + +## 3. Карта ключей + +| Key | Тип/value | TTL | +|---|---|---| +| `han:safety:rl:service:{caller}:{window}` | counter | window+jitter | +| `han:safety:text:{analysis_hash}:{rules_version}` | hot text-rules result, monitor rule ids без raw text | active config, seed ≤48h | +| `han:safety:verdict:{content_hash}:{config_version}:{detector_bundle}` | hot file verdict cache | active config, seed ≤30d | +| `han:safety:link:{url_hash}:{rules_version}:{config_version}` | stable local policy cache | active config, seed ≤48h | +| `han:safety:dns:{host_hash}:{rrtype}` | DNS answer; classification повторяется под текущей policy | actual TTL, active hard max seed 900s | +| `han:safety:wakeup` | Pub/Sub notification only | no storage | + +Raw message text, file bytes, presigned URL и task/lease documents запрещены. + +Legacy v1 stub DB2 живёт на ВМ1 и **не** создаётся на ВМ2. + +## 4. Lua scripts ВМ2 + +Обязательные: + +- rate-limit evaluate для `han:safety:rl:*`; +- при необходимости — atomic cache set с TTL/version stamp. + +Lua `safety task get+increment poll` на ВМ2 **запрещён**: poll/lease принадлежат PostgreSQL. `NOSCRIPT` reload — arch-09 §6. + +## 5. Sizing ВМ2 + +```text +Redis Safety = hot verdict/link/DNS entries + rate windows + Pub/Sub buffers +total × 1.5 allocator/fragmentation × 1.3 growth reserve +``` + +Volume `redis-data` rebuildable: бизнес-restore из dump не требуется. + +## 6. Degraded behavior ВМ2 + +При Redis недоступен: + +- production Safety продолжает task claim/poll через PostgreSQL; +- hot cache/rate/wakeup деградируют и прогреваются после восстановления; +- Redis hot cache не является core gate `/health/ready` (capability map module-05 / arch-03); +- `bitrix-sync` не деградирует из-за этого Redis. + +Restore: поднять пустой или проверенный instance; Safety прогревает cache лениво. Не копировать dump в небезопасное место. + +## 7. Metrics ВМ2 + +Общие — arch-09 §14 и [`module-09-observability-vm2.md`](module-09-observability-vm2.md). Дополнительно: Safety hot-cache hit/miss, DNS TTL cap, wakeup subscribers, rate-limit decisions caller. + +## 8. Тесты ВМ2 + +- ACL: `message_safety` видит только `han:safety:*`; `han:api:*` / `han:rt:*` недоступны; +- порт 6379 недоступен с host/public network и с ВМ1 Docker DNS; +- Safety cache loss/rebuild, DNS TTL cap и доказательство отсутствия task/lease state в Redis; +- rate Lua concurrency; +- `NOSCRIPT` reload; +- all application keys имеют TTL (кроме Pub/Sub channel); +- max value/invalid serialization; raw text/file не сохраняются; +- restart with AOF/RDB, corrupted AOF rehearsal, empty restore; +- Redis outage не останавливает PostgreSQL claim/poll; +- logs/metrics не содержат secret/value/PII/message text. + +## 9. Definition of Done ВМ2 + +Дополнительно к arch-09 §15: + +- prefixes `han:safety:*` реализованы; task/lease keys отсутствуют; +- cache loss/rebuild и DNS TTL cap доказаны; +- health/degraded §6 согласованы с capability readiness Safety; +- dashboards/alerts Redis Safety готовы; +- `MESSAGE_SAFETY_REDIS_URL` не появляется в репозитории/Compose ВМ1. + +## 10. TBD ВМ2 + +- R1/R2: maxmemory и eviction после load profile Safety. +- R4: Safety task TTL/recovery margin — в PostgreSQL / [`module-05-message-safety.md`](module-05-message-safety.md), не в Redis. +- R3: имя `MESSAGE_SAFETY_REDIS_URL` — arch-04. + +## 11. Ссылки + +- Контракт: [`arch-09-redis.md`](../../architectory/arch-09-redis.md). +- ВМ1: [`module-04-redis-vm1.md`](../../VM1_app/documentation/module-04-redis-vm1.md). +- Указатель: [`module-04-redis.md`](module-04-redis.md). +- Safety: [`module-05-message-safety.md`](module-05-message-safety.md). diff --git a/modules/module-05-message-safety.md b/VM2_services/documentation/module-05-message-safety.md similarity index 97% rename from modules/module-05-message-safety.md rename to VM2_services/documentation/module-05-message-safety.md index f17a20e..dbfa419 100644 --- a/modules/module-05-message-safety.md +++ b/VM2_services/documentation/module-05-message-safety.md @@ -1,7 +1,7 @@ # module-05. Проектная спецификация `message-safety` > Статус: нормативная постановка целевой production-реализации v2, готовая к разработке после прохождения Definition of Ready (§18). Текущий v1 stub остаётся test-only до отдельного cutover. -> Канонические источники: [`README.md`](../architectory/README.md), [`arch-00-glossary.md`](../architectory/arch-00-glossary.md), [`arch-01-system-architecture.md`](../architectory/arch-01-system-architecture.md), [`arch-02-api-contracts.md`](../architectory/arch-02-api-contracts.md), [`arch-03-docker-compose-blueprint.md`](../architectory/arch-03-docker-compose-blueprint.md), [`arch-04-settings-and-content.md`](../architectory/arch-04-settings-and-content.md), [`arch-05-agent-development-process.md`](../architectory/arch-05-agent-development-process.md), [`module-01-api-backend.md`](module-01-api-backend.md). +> Канонические источники: [`README.md`](../../architectory/README.md), [`arch-00-glossary.md`](../../architectory/arch-00-glossary.md), [`arch-01-system-architecture.md`](../../architectory/arch-01-system-architecture.md), [`arch-02-api-contracts.md`](../../architectory/arch-02-api-contracts.md), [`arch-03-docker-compose-blueprint.md`](../../architectory/arch-03-docker-compose-blueprint.md), [`arch-04-settings-and-content.md`](../../architectory/arch-04-settings-and-content.md), [`arch-05-agent-development-process.md`](../../architectory/arch-05-agent-development-process.md), [`arch-06-service-hosting-security.md`](../../architectory/arch-06-service-hosting-security.md), [`module-01-api-backend.md`](../../VM1_app/documentation/module-01-api-backend.md). ## 1. Назначение и приоритет @@ -777,7 +777,7 @@ Telemetry collector unavailable не влияет на safety verdict и readine } ``` -Core `HTTP 503 status=not_ready` используется только для invalid config/rules/PostgreSQL. Потеря worker heartbeat делает `worker=unavailable` и `files=unavailable`, но сохраняет text/links; ClamAV/S3 down выключает только `files`; DNS down — только `links`; Redis down прогревается из PostgreSQL и не выключает core. Каждый POST повторно проверяет требуемую capability и остаётся источником correctness; api-backend может кэшировать health snapshot не дольше 5 с только для fast-fail. Health доступен только через private SG ВМ1/ops, не требует service token и не раскрывает credentials/hostnames. +Core `HTTP 503 status=not_ready` используется только для invalid config/rules/PostgreSQL. Потеря worker heartbeat делает `worker=unavailable` и `files=unavailable`, но сохраняет text/links; ClamAV/S3 down выключает только `files`; DNS down — только `links`; Redis down прогревается из PostgreSQL и не выключает core. Каждый POST повторно проверяет требуемую capability и остаётся источником correctness; api-backend может кэшировать health snapshot не дольше 5 с только для fast-fail. Через private nginx ВМ2 endpoint доступен как exact `GET /internal/safety/status`, который проксируется в `/health/ready`; прямой `/health/ready` остаётся локальным container health. Private alias ограничен SG/source allow-list, не требует service token и не раскрывает credentials/hostnames. В MOCK health всегда явно возвращает `processing_mode=mock`, `mock_policy.text=allow|deny`, `mock_policy.file=allow|deny` и `status=degraded`, даже если forced responses доступны. Normal pipeline dependencies показываются как `bypassed`, не `ok`. Active alert не закрывается до возврата в `standard`. @@ -1085,10 +1085,10 @@ flowchart LR ### 19.5. Сетевые и эксплуатационные требования ВМ2 -- только private IP; публичный ingress запрещён; +- отдельный public IP/host nginx ВМ2 допускает только ACME/redirect policy на `80` и два exact CRM webhook на `443`; public Safety, generic `/internal/*`, admin и health запрещены; - inbound: private TCP 8443 только с SG ВМ1/ops; server-auth TLS internal CA + service token; - outbound по container identity: worker→PostgreSQL/S3/DNS; `freshclam`→signature CDN; `bitrix-sync`→Bitrix24; collector→SigNoz; -- health `/health/live` и `/health/ready` доступны api-backend и ops из private network; +- capability health Safety доступен api-backend и ops как exact `/internal/safety/status` на private `8443`; container `/health/live` и `/health/ready` наружу не публикуются; - при недоступности ВМ2 `api-backend` возвращает `503 dependency_unavailable`, не отправляет сообщения в Bitrix. ВМ2 — принятый SPOF MVP. Initial sizing: 4 vCPU, 8 GiB RAM, 80 GiB SSD. Меры: immutable images, PG/PITR durable truth, rebuildable Redis, resource/PID limits, queue backpressure, alerts, reprovision/restore rehearsal и RTO ≤4h. Scale-out/ВМ3 рассматриваются при sustained CPU/RAM >70%, queue age >30 с, провале performance gates §15.4, contention `bitrix-sync` или необходимости независимого release cadence. diff --git a/modules/module-07-bitrix-sync.md b/VM2_services/documentation/module-07-bitrix-sync.md similarity index 98% rename from modules/module-07-bitrix-sync.md rename to VM2_services/documentation/module-07-bitrix-sync.md index aed12e1..6f846e1 100644 --- a/modules/module-07-bitrix-sync.md +++ b/VM2_services/documentation/module-07-bitrix-sync.md @@ -2,8 +2,8 @@ > Статус: целевая постановка первого функционального релиза CRM-синхронизации. > Заменяет прежнюю спецификацию DB-connectivity stub. -> Исходный концепт: [`sync-service-concept.md`](sync-service-concept.md). -> Связанные контракты: [`../architectory/arch-01-system-architecture.md`](../architectory/arch-01-system-architecture.md), [`../architectory/arch-02-api-contracts.md`](../architectory/arch-02-api-contracts.md), [`module-01-api-backend.md`](module-01-api-backend.md), [`module-10-deployment-runbook.md`](module-10-deployment-runbook.md). +> Исторический, неканонический концепт: [`archive/sync-service-concept.md`](../../archive/sync-service-concept.md); для реализации использовать только настоящий документ и architectory. +> Связанные контракты: [`arch-01-system-architecture.md`](../../architectory/arch-01-system-architecture.md), [`arch-02-api-contracts.md`](../../architectory/arch-02-api-contracts.md), [`module-01-api-backend.md`](../../VM1_app/documentation/module-01-api-backend.md), [`module-10-deployment-vm2.md`](module-10-deployment-vm2.md). ## 1. Назначение и границы diff --git a/VM2_services/documentation/module-09-observability-vm2.md b/VM2_services/documentation/module-09-observability-vm2.md new file mode 100644 index 0000000..4736086 --- /dev/null +++ b/VM2_services/documentation/module-09-observability-vm2.md @@ -0,0 +1,193 @@ +# module-09-vm2. Наблюдаемость ВМ2 Processing + +> Статус: целевая спецификация реализации наблюдаемости на ВМ2. +> Канонический контракт (JSON-лог, redaction, sampling, Collector pipeline, SigNoz) — [`arch-07-observability.md`](../../architectory/arch-07-observability.md). Его поля, labels и `service.namespace` здесь не переопределяются. +> Контур ВМ1 в этот документ не входит: [`module-09-observability-vm1.md`](../../VM1_app/documentation/module-09-observability-vm1.md). Корреляция сквозного запроса — по `request_id` / `trace_id`. + +## 1. Назначение и границы + +Документ задаёт, **что агент ВМ2 реализует в Compose, коде, тестах и алертах этой машины**. + +ВМ2 владеет nginx (public CRM webhook + private `8443`), `message-safety`, `bitrix-sync`, `clamd`/`freshclam`, Redis Safety и локальным Collector. Guest bootstrap, Keycloak, `api-backend` и `bitrix-local-app` живут на ВМ1. ВМ2 не использует Docker hostname collector ВМ1. + +Агент ВМ2 не добавляет scrape, дашборды и алерты сервисов ВМ1. + +## 2. Сервисы и `service.name` + +| Компонент | `service.name` | +|---|---| +| nginx public/private | `nginx` | +| `message-safety` API/worker | `message-safety` | +| `bitrix-sync` | `bitrix-sync` | +| Redis Safety | `redis` | +| local Collector | `otel-collector` | + +`clamd` / `freshclam` покрываются host/container metrics и сигналами Safety (signature age, scan lanes), отдельное `service.name` в реестр arch-07 не добавляется без явного решения. + +Различать экземпляр от ВМ1 через `host.name` / `service.instance.id`. + +## 3. Collector на ВМ2 + +- отдельный экземпляр в root Compose ВМ2, собственный volume `otel-queue`; +- приложения ВМ2 экспортируют OTLP только в `otel-collector:4317` этой машины; +- export в SigNoz `192.168.0.5:4317`; hostname collector ВМ1 запрещён; +- pipeline, processors, limits, `otel-queue-init` и fail-open — arch-07 §3, §13, §14. +- telemetry outage fail-open для Safety readiness, но создаёт alert; business fail-open не отменяет Safety fail-closed на содержимом. + +Scrape targets ВМ2 (кроме самого Collector): Redis Safety exporter, nginx exporter, сервисные `/metrics` `message-safety` и `bitrix-sync`, если они не идут OTLP. + +## 4. Instrumentation + +Правила FastAPI/HTTPX/PG/Redis/S3/workers, JSON access log nginx и hostmetrics — arch-07 §8. Ниже только покрытие ВМ2. + +### 4.1. `message-safety` + +- server spans API с route template; poll `202` не маскирует финальный verdict; +- worker spans claim/process/finalize и span links на origin `request_id` / `trace_id` caller; +- child spans: PostgreSQL Safety schema, Redis Safety, S3 quarantine, ClamAV/DNS classification без file content и raw URL; +- `/health/live` исключить из traces; capability/readiness — metrics и sampled logs; +- stub mode (`400`, non-sticky) маркируется как `stub`; production SLO Safety на stub недостоверен. + +### 4.2. `bitrix-sync` + +- server spans webhook (после nginx allow-list); +- worker/batch/CRM client spans; Bitrix может не вернуть context — span закрывается результатом; +- async queue — span link, не подмена долгого worker trace; +- query token, raw payload и download URL не попадают в attributes. + +### 4.3. nginx ВМ2 + +Access log по arch-07 §8.4. Route class — bounded set: exact CRM webhook, ACME/redirect, private Safety. Query/body webhook не логируются. Метрика `webhook_rejected_total{receiver,reason="source_ip"}` формируется на nginx, потому что запрещённый запрос до upstream не доходит. + +Без native OTEL module первый server span создаёт `message-safety` или `bitrix-sync`. + +### 4.4. Redis Safety и PostgreSQL + +Exporter и ACL — arch-07 §8.2–8.3. Клиентские pool/queue metrics публикуют Safety и `bitrix-sync`. Keys/values, file bytes и message text не экспортируются. + +### 4.5. Host/Docker ВМ2 + +CPU, memory, disk, network, restarts/OOM, Docker daemon, clock sync — arch-07 §8.5. Отдельно контролировать `clamd` restarts, signature age и scan lane saturation. + +## 5. Метрики бизнес-потоков ВМ2 + +### Message Safety + +- checks/verdicts по `allow|deny|pending|error`; +- `processing_mode`, active/used `config_version`, config activation result, `message_safety_mock_enabled` и forced outcomes по `text|file`/`allow|deny`; +- poll duration/count buckets, timeout и recovery backlog age; +- stub mode info и terminal `400` отдельно, пока действует test-only контракт; +- cache hit, Redis latency, task expired/not-found. + +### Files / S3 со стороны Safety + +- quarantine object age/orphans; +- checksum/MIME/size reject; +- S3 dependency latency/error by operation and logical bucket. + +Init/complete/promote/presigned download со стороны API — спецификация ВМ1. + +### `bitrix-sync` + +- queue depth/oldest age, workflow/command transitions; +- CRM batch latency/subcommand outcome; +- limiter/throttle, retry/DLQ; +- webhook/reconciliation lag; +- mapping invariants и business-alert SLA; +- nginx `webhook_rejected_total{receiver,reason="source_ip"}`. + +До module-07 preflight Queue/CRM SLI не включаются; dashboard показывает `sync_disabled`, а не синтетический CRM success. + +UUID/user/session/dialog/task/message id не labels. + +## 6. Dashboards ВМ2 + +В SigNoz, с filter `host.name` / environment ВМ2: + +1. **nginx ingress ВМ2**: RPS, 4xx/5xx, upstream latency/status, TLS, cache, **CRM webhook** (accept/reject by source IP, method, rate limit). Без guest API/WS — это ВМ1. +2. **message-safety**: capabilities, verdicts, `202` poll, PG queue age/leases/fencing, ClamAV/signature age, file/link cache и DNS dependency. Stub явно маркируется. +3. **bitrix-sync**: mode/readiness, queue depth/oldest age, workflow/command transitions, CRM batch latency/subcommand outcome, limiter/throttle, retry/DLQ, webhook/reconciliation lag, mapping invariants и business-alert SLA. До cutover — `sync_disabled`. +4. **Redis Safety**: memory/evictions/AOF/latency/clients/keyspace. + +Сквозные Executive/SLO, Business flow и Collector health — arch-07 §10. + +## 7. Alerts ВМ2 + +Политика SLO — arch-07 §11. Ниже alerts, которые закрывает on-call ВМ2. + +### Paging + +- oldest safety task >5 минут либо Safety DLQ >0; +- Message Safety active config missing/invalid, referenced artifact unavailable или config refresh stale >5 с; +- `message_safety_mock_enabled=1` — active page/high-severity alert без auto-resolve по времени; закрывается только после возврата в standard; +- bitrix-sync invalid/revoked credential или обязательная configuration/grant missing; +- bitrix-sync technical DLQ >0, mapping invariant violation или worker/limiter heartbeat stale; +- bitrix-sync queue oldest age >30 с при healthy CRM либо sustained рост; +- Contact webhook/reconciliation cursor lag выше двух configured intervals; +- Redis Safety unavailable, AOF error или sustained evictions; +- Collector ВМ2 exporter queue >80%, dropped/refused telemetry >0 sustained; +- TLS expiry public webhook host и private `8443` <14 дней warning, <7 дней page; +- disk/OOM/restart loop ВМ2; `clamd` restart loop / stale signatures по runbook module-05. + +### Ticket/warning + +- p95 Safety poll/scan regression 20% release-over-release; +- bitrix-sync CRM last success stale, rate-limit errors выше baseline или business alert SLA overdue; +- quarantine orphan growth; +- cardinality/ingest growth >2× baseline на сериях ВМ2. + +Keycloak login, guest bootstrap и edge API 5xx ВМ1 — не алерты репозитория ВМ2. + +## 8. Docker Compose ВМ2 + +Root Compose включает `otel-collector` по arch-07 §14. Сети приложений ВМ2: `backend` + `observability`; nginx — свои public/private сети по arch-03 / [`module-03-nginx-vm2.md`](module-03-nginx-vm2.md). + +Optional profile `observability-local` на ВМ2 по умолчанию выключен: малая VM, extra RAM/disk не закладываются. + +## 9. Runbooks ВМ2 + +Общие (Collector not-ready, missing telemetry, remote outage, cardinality, PII) — arch-07 §16. + +### Высокая latency сообщения (hop ВМ2) + +1. Найти task/span по `request_id` caller. +2. Разделить Safety API, worker lease, ClamAV/DNS, S3 quarantine, sync queue. +3. Проверить circuit, queue age, PG leases/fencing и Redis Safety. +4. Не повторять ambiguous scan/send без исходного idempotency key. +5. Следовать runbook [`module-05-message-safety.md`](module-05-message-safety.md) / [`module-07-bitrix-sync.md`](module-07-bitrix-sync.md). +6. Delay до Safety (nginx/API ВМ1) — инцидент владельца ВМ1. + +### MOCK включён в production-like + +1. Подтвердить `message_safety_mock_enabled=1` и paging alert. +2. Не auto-resolve по времени. +3. Вернуть standard через root-owned five-command helper; проверить persistent alert closed. +4. Canary allow/deny после возврата. + +## 10. Definition of Done ВМ2 + +Дополнительно к arch-07 §17: + +- Collector ВМ2 validate + up в root Compose; hostname collector ВМ1 не используется; +- инструментированы `message-safety` API/worker и `bitrix-sync`; +- nginx JSON parsing, webhook reject metric и private `8443` correlation проверены; +- Redis Safety, host/Collector, ClamAV signature age metrics доступны; +- дашборды и alerts §6–§7 provisioned либо явно TBD до SigNoz packaging; +- stub/MOCK маркируются; production Safety SLO не объявляется на stub; +- до sync cutover dashboard `sync_disabled`; +- canary secret/PII/message body/file content/presigned URL отсутствуют в сигналах ВМ2; +- request с известным `request_id` находится в Safety spans; сквозной nginx ВМ1 → API → Safety — [`arch-10-deployment.md`](../../architectory/arch-10-deployment.md) §11 / [`module-10-deployment-vm2.md`](module-10-deployment-vm2.md). + +## 11. TBD и конфликты, принадлежащие ВМ2 + +- Conflict module-05: test-only `400` / non-sticky verdict. Dashboards маркируют `stub`; production SLO Safety недостоверен. +- Conflict module-07: до preflight/cutover показывать `sync_disabled`, не синтетический CRM success. +- O-TBD5 в части nginx OTEL module pinned image ВМ2. + +## 12. Ссылки + +- Контракт: [`arch-07-observability.md`](../../architectory/arch-07-observability.md). +- ВМ1: [`module-09-observability-vm1.md`](../../VM1_app/documentation/module-09-observability-vm1.md). +- Указатель: [`module-09-observability.md`](module-09-observability.md). +- Safety / sync / nginx: [`module-05-message-safety.md`](module-05-message-safety.md), [`module-07-bitrix-sync.md`](module-07-bitrix-sync.md), [`module-03-nginx-vm2.md`](module-03-nginx-vm2.md). +- Деплой: [`module-10-deployment-vm2.md`](module-10-deployment-vm2.md). diff --git a/VM2_services/documentation/module-10-deployment-vm2.md b/VM2_services/documentation/module-10-deployment-vm2.md new file mode 100644 index 0000000..557fa11 --- /dev/null +++ b/VM2_services/documentation/module-10-deployment-vm2.md @@ -0,0 +1,185 @@ +# module-10-vm2. Runbook развёртывания ВМ2 Processing + +> Статус: целевой runbook репозитория ВМ2. +> Общий контракт (VPC/SG, PG, S3, роли `deploy`, TLS процедура, порядок cutover) — [`arch-10-deployment.md`](../../architectory/arch-10-deployment.md). +> ВМ1 — [`module-10-deployment-vm1.md`](../../VM1_app/documentation/module-10-deployment-vm1.md). Не переносить команды ВМ1 и не шарить Compose/IAM/secrets. + +## 1. Границы + +ВМ2 владеет nginx public `80/443` (только exact CRM webhook) и private `8443` (Message Safety), `message-safety` API/worker, `clamd`/`freshclam`, `bitrix-sync`, Redis Safety, Collector. Guest API, Keycloak, SPA и SMS callback здесь не разворачиваются. + +`` / `` — репозиторий ВМ2. Public ACME host — ``. Private DNS `processing.internal` не публикуется. + +## 2. Sizing и load gates + +Начальный sizing без local Grafana: 4 vCPU, 8 ГБ RAM, 80 ГБ SSD, 4 ГБ swap; Redis 512 МиБ; Collector 512 МиБ + 5–10 ГБ queue; свободный диск ≥30%. + +До real traffic — load test module-05 §15.4: + +- sustained 10 text checks/s: p95 ≤2 с, p99 ≤5 с; +- sustained 2 file checks/s на 5 worker slots: среднее processing ≤2.5 с, p95 ≤60 с, public wait ≤300 с; +- 100 pending принимаются; 101-й file POST — retryable `503` без новой task; +- RPS overflow — `429 + Retry-After`; +- long Safety poll не блокирует WS/read API ВМ1; +- если gate не пройден — увеличить slots/CPU/clamd scan lanes; production traffic не открывать. + +Monthly availability SLO Safety в MVP не задаётся. Scale-out/ВМ3: sustained CPU/RAM >70%, queue age >30 с, провал performance gates, contention `bitrix-sync` или независимый release cadence. Workers масштабируются первыми по queue depth, `clamd` — scan lanes. + +## 3. Hardening и egress + +Arch-10 §5 / arch-06. Public Docker ports — `80,443`. Private `8443` не internet SG. + +Default-deny egress. Bootstrap window для registry/OS, затем закрыть. Оставить: S3, approved Bitrix portal (`bitrix-sync`), signature CDN (`freshclam`), DNS/NTP, SigNoz `4317`, PG. Постоянный open egress запрещён. + +Отдельный IAM principal Selectel: только VM2 secret names. + +## 4. Release layout и `.env` ВМ2 + +Checkout exact SHA. Структура: root Compose, `nginx`, Safety, `bitrix-sync`, Redis Safety, `observability`. Frontend-static нет. + +Секреты ВМ2 (не в `.env` ВМ1): + +- PG DSN роли Safety/`bitrix_sync` + CA; +- `MESSAGE_SAFETY_REDIS_URL`; +- Safety mode file `/etc/han-chat/message-safety-mode.env` (не repository `.env`); +- S3 **read-only quarantine** credentials; +- internal TLS PEM `8443`; +- Bitrix sync webhook tokens, portal credentials, `BITRIX_WEBHOOK_ALLOWED_CIDRS`; +- OTEL exporter secrets. + +`BITRIX_SYNC_ENABLED=false` до §9. Paired tokens local app ВМ1 здесь не живут. + +## 5. Images + +Один immutable image API+worker Safety, отдельные containers. MVP: 1 API + 1 worker с 5 file-worker slots. Build без production secrets. Frontend на ВМ2 не собирается. + +## 6. Root Compose ВМ2 + +Сервисы: public/private nginx, `message-safety-api`, `message-safety-worker`, `clamd`, `freshclam`, `bitrix-sync`, Redis Safety, local `otel-collector`. + +Networks: `public` (только nginx webhook/ACME), `backend`, `egress` (freshclam, bitrix-sync, Safety worker → S3/PG/DNS, collector → SigNoz), `observability`. Safety API/clamd/Redis без общего internet egress. + +Volumes: Redis Safety data (rebuildable), ClamAV signatures, ACME public, `otel-queue` + init. Frontend-static нет. Published: nginx 80/443; `8443` только SG ВМ1/ops. + +Compose gate — arch-10 применительно к этому Compose. + +## 7. TLS ВМ2 + +Public: arch-10 §9, `-d `. HTTP webhook paths — без redirect с query token ([`module-03-nginx-vm2.md`](module-03-nginx-vm2.md)). + +Private `8443`: сертификат internal CA, SAN = private DNS. Не Let's Encrypt. Caller ВМ1 проверяет CA bind. + +## 8. Миграции Safety + +Stub не создаёт PG tables. Production v2: schema `message_safety`, lease/fencing smoke. Первая migration создаёт `config_versions` и seed version 1; readiness без ровно одной valid active version не открывается. + +Config-only rollout — отдельный root-owned job под config-admin role: immutable draft, JSON Schema, artifacts, approvals, транзакционный activate. Runtime — только `SELECT` к config. Rollback не реактивирует retired row: клон в новую monotonic version. + +## 9. Controlled rollout `bitrix-sync` + +До `BITRIX_SYNC_ENABLED=true`: + +1. custom Contact fields и env names; universal CRM `UF_CRM_` → `ufCrm_`; +2. smart process конфликтов и `bitrix_sync.settings`; +3. входящий webhook техпользователя, права module-07 §13; +4. два HTTP-webhook на `https:///bitrix/sync/webhook/...?token=...`; query/body не в журналах; local app handler не создавать; +5. expand migrations `han_app` (делает ВМ1/api-backend) затем `bitrix_sync`; GRANT и negative tests; +6. `cutover_watermark`; отменить pending до него с `initial_full_sync_cutover`; +7. не создавать backfill; +8. image с sync disabled: `/health/live`, `sync_disabled`, smoke Bitrix methods; +9. открыть на nginx ВМ2 только два exact routes для `BITRIX_WEBHOOK_ALLOWED_CIDRS`; valid/invalid source IP; нет запросов на ВМ1; +10. включить sync; canary post-watermark; нет CRM ID в App DB; +11. canary window: queue age, 429, DLQ, webhook lag, reconciliation, source-IP rejects. + +Rollback: закрыть webhook или `503`; `BITRIX_SYNC_ENABLED=false`; ВМ1 не изменяется; mapping не удалять; schema не downgrade. + +### Изменение source IP Битрикс24 + +Сигнал: всплеск Contact из reconciliation + `webhook_rejected_total{reason="source_ip"}`. + +1. Сопоставить bounded-retention журнал rejects; query/body не извлекать. +2. Подтвердить адрес как инфраструктуру Битрикс24; query token сам по себе недостаточен. +3. Минимальный CIDR в version-controlled allow-list, review, `nginx -t` через deployment unit. +4. Safe reload; приём webhook без query/body в logs. +5. Rejects прекратились, lag нормализовался. +6. Ошибочное расширение — rollback allow-list. Автодобавление IP запрещено. + +## 10. Ordered startup ВМ2 + +1. Redis Safety, `otel-queue-init`, Collector; +2. `clamd`/`freshclam`, Safety API/worker, `bitrix-sync`; +3. nginx последним: оба TLS, exact webhook, capability health, signature age, negative ingress/egress. + +До enablement `bitrix-sync` — `sync_disabled`. Safety v2 capability `text|links|files|worker`. Redis Safety не core gate readiness. + +## 11. Public smoke ВМ2 + +```bash +curl -I http:/// +curl -i https:///internal/sync/v1/status +curl -i https:///bitrix/sync/webhook/contact +openssl s_client -connect :443 -servername +``` + +Expected: HTTP redirect/ACME policy; internal 404; GET webhook 405/404; отдельный cert. POST webhook — отдельный form-urlencoded test allowed/denied IP без query token в history. + +Private `8443` проверяется с ВМ1/ops, не из интернета. + +## 12. Observability ВМ2 + +[`module-09-observability-vm2.md`](module-09-observability-vm2.md) + arch-07. Paging: MOCK, Safety config, sync DLQ, queue age. + +## 13. Cutover, rollback, reprovision + +Cutover gates: private TLS chain/SAN; Safety v2 PG migration и lease/fencing smoke; capability `text|links|files|worker`; S3 Gate 4 (Safety read-only); performance §2; egress negative tests. Approvals: Safety Service, Rule Pack, Security, Product, Operations. Только после них ВМ1 переключает `MESSAGE_SAFETY_URL`. + +При потере ВМ2 fail-open запрещён. Reprovision из immutable image; отдельный IAM; Redis пустой; gates повторяются. RTO ≤4 ч; restore rehearsal ≥2 раза в год. + +Rollback ВМ2 не требует изменения nginx ВМ1. Caller rollback — runbook ВМ1. + +## 14. Emergency MOCK и Freshclam + +`deploy` без root login: + +```bash +sudo /usr/local/sbin/han-message-safety-mode mock --text-free true --file-free true +sudo /usr/local/sbin/han-message-safety-mode mock --text-free true --file-free false +sudo /usr/local/sbin/han-message-safety-mode mock --text-free false --file-free true +sudo /usr/local/sbin/han-message-safety-mode mock --text-free false --file-free false +sudo /usr/local/sbin/han-message-safety-mode standard +``` + +Helper `root:root 0755`; sudoers только этот executable. Config `root:han-message-safety 0640`, GID `10001`. Нет auto-expiry; incident не закрывать без `standard` и canary. File в MOCK — `scan_status=bypassed`, не `clean`. `deploy` не пишет Compose/Docker. + +`freshclam`: controlled egress только к signature CDN. Seed `max_signature_age_hours=240`, schema max `720`. Stale/failed update выключает только `files` и alert. Новая база — integrity/EICAR, atomic activate; regression — предыдущая валидная база. + +## 15. Rollback, ops, incidents ВМ2 + +Redis Safety restore — empty/clean, lazy cache ([`module-04-redis-vm2.md`](module-04-redis-vm2.md)). Routine: signature age, Safety queue, sync DLQ/webhook lag, Collector, disk, egress still deny. + +Incident: Safety timeout — checkpoint, не новый task id; Bitrix down — circuit/DLQ, webhook `503`; MOCK page — вернуть standard; cert processing host и private CA. + +Потеря ВМ2: новая VM в VPC, hardening, private DNS, secrets из VM2 IAM, empty Redis, Safety migrations, nginx webhook+8443, **не** менять public routes ВМ1 до готовности; затем повторить cutover gates. + +## 16. Definition of Done ВМ2 + +Дополнительно к arch-10 §12: + +- Compose/nginx/Redis Safety/Collector прошли профильные compose/nginx gates; +- private `8443` fail-closed до cutover, затем только источники ВМ1; +- webhook allow-list и preflight `BITRIX_SYNC_ENABLED` согласованы; +- capability/load gates module-05; +- MOCK helper и Freshclam rehearsal; +- observability ВМ2 + redaction (нет query/body/file content); +- cutover §13 не объявлен выполненным только документацией. + +## 17. TBD ВМ2 + +D-TBD5 Safety v2; D-TBD6 bitrix-sync cutover; performance gates уже в §2. + +## 18. Ссылки + +- Контракт: [`arch-10-deployment.md`](../../architectory/arch-10-deployment.md). +- ВМ1: [`module-10-deployment-vm1.md`](../../VM1_app/documentation/module-10-deployment-vm1.md). +- Указатель: [`module-10-deployment-runbook.md`](module-10-deployment-runbook.md). +- Safety / sync / nginx: [`module-05-message-safety.md`](module-05-message-safety.md), [`module-07-bitrix-sync.md`](module-07-bitrix-sync.md), [`module-03-nginx-vm2.md`](module-03-nginx-vm2.md). diff --git a/VM3_signoz/.gitignore b/VM3_signoz/.gitignore new file mode 100644 index 0000000..421904f --- /dev/null +++ b/VM3_signoz/.gitignore @@ -0,0 +1,33 @@ +# Local deployment configuration and secrets +backend/.env +backend/secrets/ +*.pem +*.key +*.p12 + +# Python +__pycache__/ +*.py[cod] +.pytest_cache/ +.mypy_cache/ +.ruff_cache/ +.venv/ +htmlcov/ +.coverage + +# JavaScript / Expo / Playwright +node_modules/ +.expo/ +dist/ +web-build/ +playwright-report/ +test-results/ + +# Java +target/ + +# IDE and OS +.idea/ +.vscode/ +.DS_Store +Thumbs.db diff --git a/codebase/Signoz/README.md b/VM3_signoz/Signoz/README.md similarity index 100% rename from codebase/Signoz/README.md rename to VM3_signoz/Signoz/README.md diff --git a/codebase/Signoz/casting.yaml b/VM3_signoz/Signoz/casting.yaml similarity index 100% rename from codebase/Signoz/casting.yaml rename to VM3_signoz/Signoz/casting.yaml diff --git a/codebase/Signoz/docs/BACKEND_OTLP.md b/VM3_signoz/Signoz/docs/BACKEND_OTLP.md similarity index 97% rename from codebase/Signoz/docs/BACKEND_OTLP.md rename to VM3_signoz/Signoz/docs/BACKEND_OTLP.md index c1fd93b..81a71b7 100644 --- a/codebase/Signoz/docs/BACKEND_OTLP.md +++ b/VM3_signoz/Signoz/docs/BACKEND_OTLP.md @@ -360,8 +360,9 @@ SigNoz. Audit events продолжают храниться отдельно в 4. экспортировать проверенный Dashboard V2 JSON; 5. уточнить sampling, retention и alert thresholds после baseline/load test. -Канонический перечень метрик, SLO, dashboards и alert policy находится в -`modules/module-09-observability.md`. +Канонический контракт telemetry, SLO и сквозных dashboards — `architectory/arch-07-observability.md`. +Сервисные метрики, dashboards и alerts — `modules/module-09-observability-vm1.md` и +`modules/module-09-observability-vm2.md`. Указатель: `modules/module-09-observability.md`. До прохождения end-to-end и outage/recovery acceptance не используйте отсутствие ошибок в SigNoz как доказательство здоровья HAN Chat. diff --git a/codebase/Signoz/docs/MVP_DASHBOARDS_ALERTS.md b/VM3_signoz/Signoz/docs/MVP_DASHBOARDS_ALERTS.md similarity index 100% rename from codebase/Signoz/docs/MVP_DASHBOARDS_ALERTS.md rename to VM3_signoz/Signoz/docs/MVP_DASHBOARDS_ALERTS.md diff --git a/codebase/Signoz/docs/NETWORK.md b/VM3_signoz/Signoz/docs/NETWORK.md similarity index 100% rename from codebase/Signoz/docs/NETWORK.md rename to VM3_signoz/Signoz/docs/NETWORK.md diff --git a/codebase/Signoz/docs/SIGNOZ_RUNBOOK.md b/VM3_signoz/Signoz/docs/SIGNOZ_RUNBOOK.md similarity index 100% rename from codebase/Signoz/docs/SIGNOZ_RUNBOOK.md rename to VM3_signoz/Signoz/docs/SIGNOZ_RUNBOOK.md diff --git a/codebase/Signoz/scripts/00-check-vm.sh b/VM3_signoz/Signoz/scripts/00-check-vm.sh similarity index 100% rename from codebase/Signoz/scripts/00-check-vm.sh rename to VM3_signoz/Signoz/scripts/00-check-vm.sh diff --git a/codebase/Signoz/scripts/05-configure-private-network.sh b/VM3_signoz/Signoz/scripts/05-configure-private-network.sh similarity index 100% rename from codebase/Signoz/scripts/05-configure-private-network.sh rename to VM3_signoz/Signoz/scripts/05-configure-private-network.sh diff --git a/codebase/Signoz/scripts/10-install-docker.sh b/VM3_signoz/Signoz/scripts/10-install-docker.sh similarity index 100% rename from codebase/Signoz/scripts/10-install-docker.sh rename to VM3_signoz/Signoz/scripts/10-install-docker.sh diff --git a/codebase/Signoz/scripts/20-deploy-signoz.sh b/VM3_signoz/Signoz/scripts/20-deploy-signoz.sh similarity index 100% rename from codebase/Signoz/scripts/20-deploy-signoz.sh rename to VM3_signoz/Signoz/scripts/20-deploy-signoz.sh diff --git a/codebase/Signoz/scripts/30-verify-signoz.sh b/VM3_signoz/Signoz/scripts/30-verify-signoz.sh similarity index 100% rename from codebase/Signoz/scripts/30-verify-signoz.sh rename to VM3_signoz/Signoz/scripts/30-verify-signoz.sh diff --git a/codebase/Signoz/scripts/40-configure-firewall.sh b/VM3_signoz/Signoz/scripts/40-configure-firewall.sh similarity index 100% rename from codebase/Signoz/scripts/40-configure-firewall.sh rename to VM3_signoz/Signoz/scripts/40-configure-firewall.sh diff --git a/functional_blocks (business logic)/chat-requirements.md b/analytics/chat-requirements.md similarity index 100% rename from functional_blocks (business logic)/chat-requirements.md rename to analytics/chat-requirements.md diff --git a/functional_blocks (business logic)/notification-requirements.md b/analytics/notification-requirements.md similarity index 99% rename from functional_blocks (business logic)/notification-requirements.md rename to analytics/notification-requirements.md index f41bfe3..b4ba95b 100644 --- a/functional_blocks (business logic)/notification-requirements.md +++ b/analytics/notification-requirements.md @@ -1057,7 +1057,7 @@ Audit-события пишутся в `audit_events` (module-01 §9.14) по п | `arch-04-settings-and-content.md` | Seed `notification.*` (§5.10) с флагами `is_public`; новые ключи `rate_limit.notification*`; отсутствие allow-list/iframe-настройки instruction; **правило «настройка, специфичная для вида уведомления, — колонка справочника, а не ключ `app_settings`»** (§5.10) | | `arch-05-agent-development-process.md` | Уточнить разграничение `record_status` (только административное удаление) и доменного `lifecycle_status` (бизнес-завершение). Отметить, что запрет физического удаления прикладных строк придётся пересматривать при выносе истории в озеро данных (§13) | | `module-01-api-backend.md` | Таблицы §10 в §9; новые S3-префиксы в §14; триггер в §16; событие подписки в §17; новые джобы в списке workers; лимиты в §19; env в §18.3 | -| `module-03-nginx.md` | Запрет `/internal/notifications/*` снаружи; зоны rate limit; CSP `frame-src 'none'` | +| `module-03-nginx-vm1.md` | Запрет `/internal/notifications/*` снаружи; зоны rate limit; CSP `frame-src 'none'` | | `module-07-bitrix-sync.md` | Контракт задачи `document.client_uploaded`: payload, dedup, ожидаемое поведение при включении сервиса | --- diff --git a/functional_blocks (business logic)/user-requirements.md b/analytics/user-requirements.md similarity index 100% rename from functional_blocks (business logic)/user-requirements.md rename to analytics/user-requirements.md diff --git a/architectory/README.md b/architectory/README.md index 0f42776..65f716e 100644 --- a/architectory/README.md +++ b/architectory/README.md @@ -11,17 +11,21 @@ | [`arch-00-glossary.md`](arch-00-glossary.md) | Канонические имена и семантика enum/lifecycle: сущности, поля, id, enum, бакеты S3, env | | [`arch-01-system-architecture.md`](arch-01-system-architecture.md) | Общая архитектура: компоненты, сценарии, потоки данных, безопасность | | [`arch-02-api-contracts.md`](arch-02-api-contracts.md) | Реестр API-контрактов, realtime, гостевая сессия, OpenAPI, аудит | -| [`arch-03-docker-compose-blueprint.md`](arch-03-docker-compose-blueprint.md) | Требования к Docker Compose, nginx, сетям, TLS и rate limits | +| [`arch-03-docker-compose-blueprint.md`](arch-03-docker-compose-blueprint.md) | Требования к Docker Compose, сетям и published ports. Детальный контракт nginx — arch-08 | | [`arch-04-settings-and-content.md`](arch-04-settings-and-content.md) | `.env` (infra), таблица `app_settings`, значения service-token переменных, типы файлов | | [`arch-05-agent-development-process.md`](arch-05-agent-development-process.md) | Правила разработки модулей отдельными агентами | | [`arch-06-service-hosting-security.md`](arch-06-service-hosting-security.md) | Безопасность VM и деплоя: OS-роли, SSH, sudo/systemd, секреты, контейнеры, сеть и lockdown | +| [`arch-07-observability.md`](arch-07-observability.md) | Контракт наблюдаемости: Collector, JSON-логи, корреляция, redaction, sampling, SLO, SigNoz. Реализация VM — `module-09-observability-vm1.md` / `module-09-observability-vm2.md` | +| [`arch-08-nginx.md`](arch-08-nginx.md) | Контракт корневого nginx: TLS/ACME, request id, internal 404, access log, reload. Реализация VM — `module-03-nginx-vm1.md` / `module-03-nginx-vm2.md`. Сети и ports — arch-03 | +| [`arch-09-redis.md`](arch-09-redis.md) | Контракт Redis: не source of truth, формат ключей, TTL, Lua, AOF/ACL. Реализация VM — `module-04-redis-vm1.md` / `module-04-redis-vm2.md` | +| [`arch-10-deployment.md`](arch-10-deployment.md) | Контракт развёртывания: VPC/SG, PG/S3, роли `deploy`, TLS процедура, cutover. Runbook VM — `module-10-deployment-vm1.md` / `module-10-deployment-vm2.md`. OS-роли — arch-06 | ## Как читать 1. Начните с **arch-01** — общая картина и зафиксированные решения MVP. -2. При работе с API — **arch-02**; с Compose/nginx — **arch-03**; с настройками — **arch-04**; с VM, SSH, правами деплоя, секретами и host/container hardening — **arch-06**. +2. При работе с API — **arch-02**; с Compose/сетями — **arch-03**; с контрактом nginx — **arch-08** и профильная спецификация VM; с Redis — **arch-09** и профильная спецификация VM; с настройками — **arch-04**; с VM, SSH, правами деплоя, секретами и host/container hardening — **arch-06**; с rollout stages/gates — **arch-10** и профильный runbook VM; с telemetry/логами/traces — **arch-07** и профильная спецификация VM. 3. Спорные **имена** полей, id, enum, бакетов и базовая семантика enum/lifecycle — **arch-00**. Лимиты и правила реализации остаются в профильных arch-*. -4. Перед разработкой модуля — **arch-05**, релевантные разделы arch-01/arch-02 и arch-06, если меняются deployment, сети, volumes, capabilities или секреты. +4. Перед разработкой модуля — **arch-05**, релевантные разделы arch-01/arch-02 и arch-06, если меняются deployment, сети, volumes, capabilities или секреты. Наблюдаемость сервиса — arch-07 плюс `module-09-observability-vm1.md` или `module-09-observability-vm2.md`. Nginx — arch-08 плюс `module-03-nginx-vm1.md` или `module-03-nginx-vm2.md`. Redis — arch-09 плюс `module-04-redis-vm1.md` или `module-04-redis-vm2.md`. Раскатка VM — arch-10 плюс `module-10-deployment-vm1.md` или `module-10-deployment-vm2.md`. ## Приоритет документов @@ -31,9 +35,13 @@ 2. **arch-01** — границы сервисов, сценарии, sync, безопасность. 3. **arch-02** — HTTP-контракты и направление вызовов. 4. **arch-06** — безопасность размещения на VM, OS-роли, SSH, secrets delivery, host/container hardening и production-деплой. -5. **arch-03** — Compose, сети контейнеров, nginx и TLS. -6. **arch-04** — non-secret env, secret references, `app_settings`, публичные DTO. -7. **arch-05** — процесс разработки. +5. **arch-03** — Compose, сети контейнеров и published ports. +6. **arch-08** — контракт nginx: TLS/ACME, request id, internal 404, access log, reload. Routing matrix — профильный module-03 VM spec. +7. **arch-04** — non-secret env, secret references, `app_settings`, публичные DTO. +8. **arch-09** — контракт Redis: не source of truth, ключи, TTL, Lua, AOF/ACL. Карта ключей — профильный module-04 VM spec. +9. **arch-07** — контракт telemetry: JSON-поля, resource attributes, redaction, sampling, Collector, SigNoz. Не бизнес-лимиты сервисов. +10. **arch-10** — контракт развёртывания: VPC/SG, PG/S3, stages/gates, cutover. Не ослабляет arch-06. Процедуры VM — профильный module-10 runbook. +11. **arch-05** — процесс разработки. Профильные спецификации модулей уточняют реализацию внутри этих границ. Если границы не позволяют эффективно реализовать модуль, то агент, разрабатывающий модуль, может предложить внести изменения в архитектуру. @@ -42,15 +50,19 @@ - Имена полей, бакетов, статусов → **arch-00**, затем синхронизация arch-*. - Endpoint или auth → **arch-02**, при необходимости arch-01/arch-03. - Новая интеграция → сначала **arch-02**. -- Compose, nginx, TLS → **arch-03**. +- Compose, сети контейнеров, published ports → **arch-03**. +- TLS/ACME nginx, request id, internal 404, access log → **arch-08**, затем профильный `module-03-nginx-vm1.md` или `module-03-nginx-vm2.md`. +- Redis ключи/TTL/ACL/AOF, запрет очереди и OTP store → **arch-09**, затем профильный `module-04-redis-vm1.md` или `module-04-redis-vm2.md`. - VM, SSH, sudo, systemd-деплой, secret delivery, container/host hardening → **arch-06**, затем синхронизация arch-03/arch-04 и runbook. +- Rollout stages, SG/DNS, PG/S3 gates, Safety cutover порядок → **arch-10**, затем профильный `module-10-deployment-vm1.md` или `module-10-deployment-vm2.md`. +- JSON-лог, `request_id`/`trace_id`, redaction, sampling, Collector, SigNoz → **arch-07**, затем профильный `module-09-observability-vm1.md` или `module-09-observability-vm2.md`. ## В бэклоге (не MVP) | Тема | Где зафиксировано | |---|---| | Доставка документов компании из Bitrix24 в приложение (`bitrix-sync` → `api-backend`, уведомление клиента) | [`!Backlog.md`](../../HAN_chat/!Backlog.md), п. 9; arch-01 — заглушка UI «Документы» | -| Интеграция с SMS-провайдерами (отправка OTP, отключение `KEYCLOAK_OTP_MOCK_*`) | Спецификация: [`module-11-idgtl-sms.md`](../modules/module-11-idgtl-sms.md) (доставка через Direct SMS API; проверка OTP — локально в Keycloak) | +| Интеграция с SMS-провайдерами (отправка OTP, отключение `KEYCLOAK_OTP_MOCK_*`) | Спецификация: [`module-11-idgtl-sms.md`](../VM1_app/documentation/module-11-idgtl-sms.md) (доставка через Direct SMS API; проверка OTP — локально в Keycloak) | ## Каноническое размещение production-контуров @@ -66,7 +78,6 @@ | # | Пробел | Статус | |---|---|---| | G8 | Явный список `is_public=true` для ключей `app_settings` | Отложить до оформления сервисов; seed в модуле `database` | -| G9 | GRANT-модель `bitrix_sync_user` на `han_app`: таблицы, колонки, read/write границы | Закрыто в `module-07-bitrix-sync.md` §13; точный SQL реализуется migrations и проходит negative permission tests | | G10 | Полный DTO `GET /api/v1/public/app-config` и мэппинг `setting_key → response field` | Уточнить при оформлении OpenAPI `api-backend` | | G11 | Версионирование API/WS: deprecation policy, срок поддержки v1, `ws_protocol_version` | Уточнить перед публичным релизом API | | G12 | Масштабирование realtime: Redis Pub/Sub, sticky sessions, backpressure при нескольких репликах `api-backend` | Post-MVP / перед горизонтальным масштабированием | @@ -83,6 +94,10 @@ runbook. - Изменение service seed/schema/embedded artifact или feature flag, влияющего на edge route/allow-list → arch-04 + профильный module + rollout/rollback - gates в module-10. + gates в arch-10 и профильном `module-10-deployment-vm1.md` / `module-10-deployment-vm2.md`. - Новый термин / enum → arch-00, затем поиск по arch-*. +- Изменение JSON-лога, resource attributes, redaction, sampling, Collector pipeline или SigNoz endpoint → arch-07 (+ профильный module-09 VM spec, если меняется состав сервисов/алертов этой машины). +- Изменение общего контракта nginx (TLS/ACME, request id, internal 404, reload) → arch-08 (+ профильный module-03 VM spec, если меняется routing/allow-list этой машины). +- Изменение общего контракта Redis (формат ключей, TTL, Lua, AOF/ACL, запрет очереди) → arch-09 (+ профильный module-04 VM spec, если меняется карта ключей этой машины). +- Изменение общего rollout (SG/DNS, PG/S3 gates, cutover порядок) → arch-10 (+ профильный module-10 VM runbook). - Закрытие пробела → убрать из «Открытые пробелы» и отразить решение в arch-*. diff --git a/architectory/arch-01-system-architecture.md b/architectory/arch-01-system-architecture.md index e285678..1265939 100644 --- a/architectory/arch-01-system-architecture.md +++ b/architectory/arch-01-system-architecture.md @@ -20,11 +20,11 @@ HAN Chat - приложение для мигрантов, где стартов - Мультиязычность в первом релизе не нужна, но тексты должны храниться по мнемоникам для будущих переводов. - Среда на первом этапе одна и проектируется как боевая. - Вложения чата MVP: **только изображения и PDF** — см. [`arch-04-settings-and-content.md`](arch-04-settings-and-content.md), «Разрешённые типы файлов чата». -- SMS OTP вводится поэтапно: до production rollout действует явный mock (`KEYCLOAK_OTP_MOCK_ENABLED=true`); целевой real mode — Keycloak генерирует/локально проверяет OTP и создаёт durable order в `sms-service`, а worker асинхронно вызывает i-Digital Direct. Контракт и gates — [`module-11-idgtl-sms.md`](../modules/module-11-idgtl-sms.md). +- SMS OTP вводится поэтапно: до production rollout действует явный mock (`KEYCLOAK_OTP_MOCK_ENABLED=true`); целевой real mode — Keycloak генерирует/локально проверяет OTP и создаёт durable order в `sms-service`, а worker асинхронно вызывает i-Digital Direct. Контракт и gates — [`module-11-idgtl-sms.md`](../VM1_app/documentation/module-11-idgtl-sms.md). - Популярный вопрос при выборе **автоматически отправляется как сообщение**; если пользователь не авторизован — сначала согласия и OTP, затем отправка. - Notification Center v1 использует два контура: G — общие read-only гостевые кампании, P — персональные уведомления с состоянием в App DB. Виды, CTA, кнопки и палитра задаются каталогом данных. - Инструкция `install_app` всегда открывается во внешней новой вкладке; iframe/модалка для неё не используется. -- Перечень таблиц и миграций App DB проектирует модуль `database` (и владельцы схем других сервисов); arch фиксирует только **разделение схем** PostgreSQL и контракты между сервисами. +- Перечень таблиц и миграций схемы `han_app` проектирует `module-01-api-backend` и его migration owner; владельцы остальных сервисов проектируют свои схемы. Arch фиксирует только **разделение схем** PostgreSQL и контракты между сервисами. ## Пользовательские сценарии @@ -243,7 +243,7 @@ Frontend не должен: ### Bitrix24 sync service -Отвечает за асинхронную двустороннюю синхронизацию данных между App DB и Битрикс24 CRM по контракту [`../modules/module-07-bitrix-sync.md`](../modules/module-07-bitrix-sync.md): +Отвечает за асинхронную двустороннюю синхронизацию данных между App DB и Битрикс24 CRM по контракту [`module-07-bitrix-sync.md`](../VM2_services/documentation/module-07-bitrix-sync.md): - **канонический mapping** и его историю в `bitrix_sync.entity_external_mapping`; App DB не хранит CRM Contact ID; - **App DB → Bitrix24:** durable workflow для `contact.map_or_create`, `contact.update`, `contact.deactivate`; @@ -287,7 +287,7 @@ Frontend не должен: Confidential **backend client** Keycloak (client credentials) в MVP **не обязателен**: S2S между нашими сервисами идёт по service tokens, не через Keycloak. Client можно завести заранее в realm как optional для будущих admin/ops сценариев. -### Nginx Reverse Proxy +### Nginx Reverse Proxy (целевая двух-VM топология) Отвечает за: @@ -296,11 +296,11 @@ Confidential **backend client** Keycloak (client credentials) в MVP **не об - редирект HTTP на HTTPS (на веб-домене; для выделенного API-домена HTTP не допускается — см. «Принципы безопасности»); - маршрутизацию `/api/*` в api-backend (включая `WS /api/v1/realtime`); - маршрутизацию `/auth/*` или выделенного auth-домена в Keycloak; -- маршрутизацию публичных `/bitrix/*` endpoint в `bitrix-local-app`; -- маршрутизацию `/bitrix/sync/*` webhook endpoint в `bitrix-sync`; +- на nginx ВМ1 — маршрутизацию только `/bitrix/handler`, `/bitrix/install`, `/bitrix/placement` в `bitrix-local-app`; +- на отдельном public nginx ВМ2 — маршрутизацию только exact `/bitrix/sync/webhook/contact` и `/bitrix/sync/webhook/alert` в `bitrix-sync`; ВМ1 эти paths не проксирует; - маршрутизацию только exact `POST /callbacks/idgtl/sms` в `sms-service` по HTTPS, с allowlist актуального IP Direct и без логирования Basic Authorization; - защиту internal endpoint `bitrix-local-app` через private network или `nginx allowlist`; -- отсутствие публичной маршрутизации к `message-safety` — сервис доступен только из внутренней Docker-сети; +- отсутствие публичной маршрутизации к `message-safety`: `api-backend` ВМ1 вызывает private nginx ВМ2 `:8443` по HTTPS с internal CA и service token; Docker DNS/HTTP допустим только внутри ВМ2 за gateway; - передачу `X-Forwarded-For`, `X-Forwarded-Proto`, `X-Forwarded-Host`, `X-Request-ID` (если клиент не прислал `X-Request-ID` — nginx **генерирует** UUID и прокидывает upstream); - базовые лимиты размера запроса и timeout; - грубые edge rate limits по IP, route и зоне риска; diff --git a/architectory/arch-02-api-contracts.md b/architectory/arch-02-api-contracts.md index a27c1eb..fb3503a 100644 --- a/architectory/arch-02-api-contracts.md +++ b/architectory/arch-02-api-contracts.md @@ -9,8 +9,8 @@ ## Правила связности - Любой новый endpoint, webhook, worker-contract или внешний вызов сначала добавляется в этот файл; при появлении профильного документа модуля-владельца — дублируется там для детализации реализации. -- Публичные пользовательские API находятся под `/api/v1`; internal API не публикуются наружу через `nginx`. -- Internal HTTP API между backend-сервисами используют единую маску: **`/internal/{service_mnemonic}/v1/{resource}`**, где `{service_mnemonic}` — короткое имя владельца endpoint (см. [`arch-00-glossary.md`](arch-00-glossary.md), «Мнемоники internal API»). Health-check остаётся на `/health/*`. +- Публичные пользовательские API находятся под `/api/v1`; public listeners nginx `80/443` не публикуют `/internal/*`. Канонический production ingress Safety — отдельный private listener nginx ВМ2 `:8443` с internal CA, source allow-list и service token; это не public route и не Docker HTTP fallback. +- Internal HTTP API между backend-сервисами используют единую маску: **`/internal/{service_mnemonic}/v1/{resource}`**, где `{service_mnemonic}` — короткое имя владельца endpoint (см. [`arch-00-glossary.md`](arch-00-glossary.md), «Мнемоники internal API»). Утверждённое исключение — target Message Safety `/internal/safety/v2/*`; legacy `/internal/safety/v1/*` остаётся только stub до cutover и на private `:8443` не публикуется. Health-check остаётся на `/health/*`. - OpenAPI 3.1 обязателен для HTTP-контрактов `api-backend`, `message-safety`, `bitrix-sync` и `bitrix-local-app` — файлы `{service}/openapi.yaml` в репозитории сервиса (см. раздел «OpenAPI»); для Bitrix24 REST фиксируются используемые методы и payload-мэппинг. - Все service-to-service вызовы передают `X-Request-ID` и по возможности W3C `traceparent`. - Frontend передаёт **`X-Ux-Session-Id`** во всех JWT-запросах к `api-backend`, когда UX-сессия активна (рекомендуется для аналитики и логов; **не** является auth). `session-start` и `consents` требуют JWT. @@ -18,7 +18,7 @@ ## Service tokens (internal API) -Все internal endpoint (`/internal/*`) доступны **только** из Docker/VPC-сети и требуют service token. Endpoint не публикуются через `nginx` (исключение — ops внутри VPC). +Все internal endpoint (`/internal/*`) доступны **только** из Docker/VPC-сети и требуют service token. Public listeners nginx их не публикуют. Private nginx ВМ2 `:8443` является утверждённым ingress для Safety hot path и allow-listed ops endpoint внутри VPC. | Переменная | Кто проверяет | Кто передаёт | Endpoint | Заголовок | |---|---|---|---|---| @@ -114,6 +114,7 @@ | `403` | `forbidden` | Доступ запрещён и ресурс не скрывается | нет | | `404` | `not_found` | Ресурс не существует или принадлежит другому пользователю | нет | | `409` | `idempotency_key_reused` | Тот же `Idempotency-Key` с другим fingerprint | нет | +| `409` | `resource_state_conflict` | JWT валиден, но локальный `UserIdentity` ещё не создан через bootstrap, либо ресурс находится в несовместимом lifecycle state | после bootstrap либо изменения state | | `409` | `notification_conflict` | `(source, external_id)` уже занят Create с другим fingerprint | нет | | `409` | `notification_closed` | Действие по уже закрытому уведомлению | нет | | `422` | `message_blocked` | Message Safety вернул final deny | нет | @@ -126,6 +127,8 @@ Правило доступа к пользовательским ресурсам: для `dialog_id`, `message_id`, `attachment_id`, `document_id`, принадлежащих другому `user_id`, api-backend по умолчанию возвращает `404 not_found`, чтобы не раскрывать существование ресурса. `403 forbidden` используется только для операций, где сам факт ресурса уже известен пользователю или оператору. +Для любого protected endpoint, кроме самого `POST /api/v1/auth/bootstrap`, валидный JWT при отсутствии локального `UserIdentity` возвращает `409 resource_state_conflict` с generic сообщением `bootstrap required`. Frontend после такого ответа выполняет bootstrap один раз и повторяет исходную операцию с тем же idempotency key, если она идемпотентна. + ### `POST /api/v1/auth/bootstrap` (после OTP) Вызывается **один раз** после успешного OTP и получения JWT. Создаёт локального пользователя и **сразу** сохраняет согласия из тела (атомарно в одной транзакции). **Не** создаёт UX-сессию — для этого используется `POST /api/v1/analytics/session-start`. @@ -182,7 +185,7 @@ } ``` -api-backend сохраняет согласия с привязкой к **`user_id`** из JWT. Пользователь должен уже существовать (`bootstrap` выполнен), иначе **`404`** / **`409`** по контракту модуля. Обязательные согласия без `accepted: true` → **`403`** `consents_required`. +api-backend сохраняет согласия с привязкой к **`user_id`** из JWT. Пользователь должен уже существовать (`bootstrap` выполнен), иначе `409 resource_state_conflict`. Обязательные согласия без `accepted: true` → **`403`** `consents_required`. ### `POST /api/v1/analytics/session-start` (событие `session_start`) @@ -499,12 +502,46 @@ Frontend не обращается напрямую к Keycloak DB и не хр |---|---|---|---|---| | `POST /internal/safety/v2/messages/check` | `message-safety` | `api-backend` | Проверка текста, ссылок и файлов | private HTTPS + internal CA + `X-Service-Token` | | `GET /internal/safety/v2/messages/tasks/{task_id}` | `message-safety` | `api-backend` | Опрос до финального вердикта **внутри** того же public `POST .../messages` | private HTTPS + internal CA + `X-Service-Token` | +| `GET /internal/safety/status` | nginx ВМ2 → `message-safety /health/ready` | `api-backend`, ops | Короткоживущий capability snapshot; не correctness gate | private HTTPS + internal CA + source allow-list | | Read S3-quarantine | Selectel S3 | `message-safety` | Чтение файла worker-ом при cache miss | read-only key | HTTP-семантика target v2 от `message-safety`: `200 allow`, `403 deny`, `202 Accepted/pending`. Текущие `/v1/*` и `203` относятся только к legacy stub и не являются production-контрактом. +Канонический wire DTO `POST .../check`: + +```json +{"message_id":"uuid","content_kind":"text","text":"Текст сообщения","attachment":null} +``` + +```json +{ + "message_id":"uuid", + "content_kind":"file", + "text":"", + "attachment":{ + "attachment_id":"uuid", + "quarantine_object_key":"quarantine/users/{user_id}/dialogs/{dialog_id}/{attachment_id}", + "quarantine_version_id":"opaque-version-id", + "quarantine_etag":"\"etag\"", + "mime_type":"application/pdf", + "size_bytes":12345, + "checksum":"sha256:<64-lowercase-hex>" + } +} +``` + +DTO является strict discriminated union, unknown fields запрещены. Caller маппит App DB `checksum_sha256` в `attachment.checksum` с обязательным prefix `sha256:`; `quarantine_version_id` и `quarantine_etag` передаются без переименования. + Normative details v2: каждый verdict/pending содержит `processing_mode=standard|mock` и `config_version`; `202` обязательно содержит `Location`, `Retry-After`, `task_id`, `expires_at` и существует только в standard mode; terminal `503 task_failed` — `terminal=true,retryable=false`; transient `503 dependency_unavailable` — `terminal=false,retryable=true`; `409 safety_request_conflict` — non-retryable caller invariant. Все domain deny имеют `reason_code=message_blocked`. +`Location` должен быть origin-relative path `/internal/safety/v2/messages/tasks/{task_id}`. Caller и recovery job резолвят его относительно origin `MESSAGE_SAFETY_URL`; absolute URL, другой host или path вне этого prefix отклоняются как нарушение контракта без HTTP-запроса. + +CA-пара caller: `MESSAGE_SAFETY_CA_HOST_PATH` задаёт root-owned host bind, `MESSAGE_SAFETY_CA_FILE` — путь к нему внутри контейнера `api-backend`. Для remote production URL обязательны оба уровня доставки; TLS verification отключать запрещено. + +Caller budget: POST timeout `MESSAGE_SAFETY_POST_TIMEOUT_SEC=5`, одна попытка GET task — не более `2` секунд, client sync-poll budget `MESSAGE_SAFETY_TASK_POLL_MAX_SEC=300`, durable recovery budget `HAN_APP_SAFETY_RECOVERY_MAX_SEC=1200`. Числа являются initial defaults из arch-04; изменение выполняется синхронно в arch-04 и профильных модулях. + +Internal error subset не смешивается с public JWT errors: `401 service_unauthorized`, `400 validation_error`, `404 task_not_found`, `409 safety_request_conflict`, `429 rate_limit_exceeded`, `500 internal_error`, `503 dependency_unavailable|task_failed`. Поля и retry-семантика определены в `module-05` §8.3. + Emergency MOCK включается только root-owned helper/restart на ВМ2. В MOCK нет content/link/file checks и `202`: `TEXT_FREE`/`FILE_FREE=true` → sync `200`, false → canonical sync `403`. Auth/DTO/idempotency/audit/rate limits сохраняются. Public API не раскрывает `processing_mode`. Поведение `api-backend`: @@ -514,6 +551,8 @@ Emergency MOCK включается только root-owned helper/restart на 3. При `202` сохраняет `task_id`, `Location`, deadline и **не ставит задачу в свою очередь анализа**; синхронно поллит `Location`, соблюдая `Retry-After`, до `200`/`403`, terminal failed `503` или timeout. 4. Решение «проверка быстрая или долгая» — только у `message-safety`. Ожидание poll держит **одно** клиентское HTTP-соединение; это не блокирует обработку других запросов (параллельные workers/async). +`api-backend` не вызывает `/internal/sync/v1/*`. Этот ops-only namespace может находиться на том же private listener `:8443`, но защищается отдельным `BITRIX_SYNC_SERVICE_TOKEN` и path allow-list. + Checkpoint: на время poll — запись в **`safety_tasks`** (`han_app`) для recovery при crash/timeout (I1), не очередь анализа. Recovery contract для `han_app.safety_tasks`: @@ -622,7 +661,7 @@ Circuit breaker + timeout budget (I2): при открытом circuit на `mes `contact.rebind` не является задачей `han_app.sync_queue`: это audited административный workflow, создаваемый только через `bitrix_sync.request_bitrix_contact_rebind`. `bitrix-sync` **не создаёт** `UserIdentity` / `ClientProfile` в auth-flow; вход worker — задачи из `sync_queue`, созданные триггерами. -`entity_id` contact-задачи всегда равен `UserIdentity.id`; payload не содержит PII snapshot. Полный DDL/state-machine contract — [`../modules/module-07-bitrix-sync.md`](../modules/module-07-bitrix-sync.md), §§6–9. +`entity_id` contact-задачи всегда равен `UserIdentity.id`; payload не содержит PII snapshot. Полный DDL/state-machine contract — [`module-07-bitrix-sync.md`](../VM2_services/documentation/module-07-bitrix-sync.md), §§6–9. ### Internal HTTP `bitrix-sync` (ops, не hot path) @@ -707,7 +746,7 @@ Raw OTP и полный номер телефона в audit **не** пишут | `request_id` | из `X-Request-ID` | | `ip`, `user_agent` | из proxy headers | -Presigned URL и содержимое файла в audit **не** пишутся. Структура таблицы — модуль `database`. +Presigned URL и содержимое файла в audit **не** пишутся. Структура таблицы принадлежит `module-01-api-backend` и migration owner схемы `han_app`. ## Health-контракты diff --git a/architectory/arch-03-docker-compose-blueprint.md b/architectory/arch-03-docker-compose-blueprint.md index b3db15b..1fce370 100644 --- a/architectory/arch-03-docker-compose-blueprint.md +++ b/architectory/arch-03-docker-compose-blueprint.md @@ -1,6 +1,6 @@ # arch-03. Docker Compose blueprint -> Термины (имена бакетов S3, идентификаторы) — в [`arch-00-glossary.md`](arch-00-glossary.md). Контракт Message Safety Service — в [`arch-02-api-contracts.md`](arch-02-api-contracts.md), раздел «api-backend ↔ message-safety». Переменные окружения и настройки — в [`arch-04-settings-and-content.md`](arch-04-settings-and-content.md). VM/SSH, OS-роли, секреты, systemd-деплой и hardening — в [`arch-06-service-hosting-security.md`](arch-06-service-hosting-security.md). +> Термины (имена бакетов S3, идентификаторы) — в [`arch-00-glossary.md`](arch-00-glossary.md). Контракт Message Safety Service — в [`arch-02-api-contracts.md`](arch-02-api-contracts.md), раздел «api-backend ↔ message-safety». Переменные окружения и настройки — в [`arch-04-settings-and-content.md`](arch-04-settings-and-content.md). VM/SSH, OS-роли, секреты, systemd-деплой и hardening — в [`arch-06-service-hosting-security.md`](arch-06-service-hosting-security.md). Детальный контракт nginx (TLS/ACME, request id, internal 404, reload) — [`arch-08-nginx.md`](arch-08-nginx.md); routing matrix VM — [`module-03-nginx-vm1.md`](../VM1_app/documentation/module-03-nginx-vm1.md) и [`module-03-nginx-vm2.md`](../VM2_services/documentation/module-03-nginx-vm2.md). ## Назначение @@ -201,7 +201,8 @@ docker compose exec api-backend ruff format . - **API-домен** (если выделен отдельный host, напр. `api.example.ru`): **не** слушает порт `80`; только `listen 443 ssl`; HTTP-запросы к API-домену недоступны; - **единый домен MVP** (напр. `tohin.ru` с путями `/api/*`, `/auth/*`, web): считается веб-доменом; порт `80` — только redirect на HTTPS для всего server block; после редиректа весь пользовательский трафик — HTTPS; - **auth** на том же host, что API (`/auth/*`): следует политике host (redirect-only на :80 или HTTPS-only для выделенного API-host); - - **Bitrix callbacks** (`/bitrix/*`, `/bitrix/sync/*`): только HTTPS; порт `80` не обслуживает эти location — только redirect; + - **Bitrix local-app callbacks ВМ1** (`/bitrix/handler|install|placement`): только HTTPS; порт `80` — только redirect; + - **CRM sync webhook ВМ2** (exact `/bitrix/sync/webhook/contact|alert`): только HTTPS на отдельном processing host; HTTP не отражает query token в redirect; - маршрутизирует `/api/*` в `api-backend` (включая WebSocket upgrade для `/api/v1/realtime`); - маршрутизирует `/auth/*` в `keycloak` или проксирует отдельный auth-домен; - маршрутизирует публичные `/bitrix/*` endpoint в `bitrix-local-app`; @@ -293,7 +294,7 @@ paths, `clamd` health и фактического обновления signature ### bitrix-sync -Python API/worker service ВМ2 для durable двусторонней синхронизации App DB ↔ Bitrix24 CRM. Каноническая постановка — [`../modules/module-07-bitrix-sync.md`](../modules/module-07-bitrix-sync.md). +Python API/worker service ВМ2 для durable двусторонней синхронизации App DB ↔ Bitrix24 CRM. Каноническая постановка — [`module-07-bitrix-sync.md`](../VM2_services/documentation/module-07-bitrix-sync.md). Требования: @@ -362,7 +363,7 @@ Identity provider. **Обязателен** в compose-контуре с пер - включены proxy settings для работы за `nginx`; - импорт realm в local/dev; - использует managed PostgreSQL, схема `keycloak` (см. раздел «Managed PostgreSQL» выше); -- OTP mock / SMS SPI — см. arch-04; real mode вызывает `sms-service` по сети `backend`, а единственный утверждённый внешний вызов Keycloak через `egress` — server-side validation Yandex SmartCaptcha; +- OTP mock / SMS SPI — см. arch-04; real mode вызывает `sms-service` по сети `backend`; при `KEYCLOAK_YANDEX_CAPTCHA_ENABLED=true` Keycloak подключается к `egress` с destination allow-list только для server-side validation Yandex SmartCaptcha, при `false` egress у Keycloak отсутствует; - healthcheck; - взаимодействия — [`arch-02-api-contracts.md`](arch-02-api-contracts.md), «Frontend ↔ Keycloak», и [`arch-01-system-architecture.md`](arch-01-system-architecture.md), «Keycloak». @@ -386,7 +387,8 @@ Identity provider. **Обязателен** в compose-контуре с пер - поддерживать TTL для лимитных и idempotency ключей; - **не** хранить OTP counters для `api-backend` (OTP — зона Keycloak/SPI); - sync_queue, leases, limiter coordination и durable wake-up fallback хранятся в PostgreSQL; `LISTEN/NOTIFY` — только optimization, Redis sync-service не использует; -- разделение DB index (I4): см. arch-04 (`REDIS_URL`, `MESSAGE_SAFETY_REDIS_URL`). +- разделение DB index (I4): см. arch-04 (`REDIS_URL`, `MESSAGE_SAFETY_REDIS_URL`); +- детальный контракт и карта ключей — [`arch-09-redis.md`](arch-09-redis.md), [`module-04-redis-vm1.md`](../VM1_app/documentation/module-04-redis-vm1.md), [`module-04-redis-vm2.md`](../VM2_services/documentation/module-04-redis-vm2.md). ### otel-collector @@ -405,7 +407,8 @@ Identity provider. **Обязателен** в compose-контуре с пер - ВМ1 `public`: edge nginx, Keycloak proxy и frontend entrypoint. - ВМ1 `backend`: `api-backend`, `bitrix-local-app`, Keycloak, SMS API и Redis DB0/DB1. - ВМ2 `backend`: nginx, Safety API/worker, `bitrix-sync`, `clamd` и Redis Safety. -- `egress` подключается только к процессам с назначением: `freshclam` → signature CDN; `bitrix-sync` → утверждённый Bitrix portal; Safety worker → S3/PG/DNS; collector → private SigNoz. Общего internet egress у Safety API/clamd/Redis нет. +- ВМ1 `egress` подключается только к процессам с назначением: `sms-worker` → i-Digital Direct; Keycloak → SmartCaptcha только при включённом feature flag; `api-backend` → S3 и private PG, а вызов Safety идёт к `processing.internal:8443`; local collector → private SigNoz. `sms-service` без совмещённого worker, `bitrix-local-app` и Redis не получают общий internet egress. +- ВМ2 `egress` подключается только к процессам с назначением: `freshclam` → signature CDN; `bitrix-sync` → утверждённый Bitrix portal; Safety worker → S3/PG/DNS; collector → private SigNoz. Общего internet egress у Safety API/clamd/Redis нет. - `observability` существует отдельно на каждой VM и ведёт в её local collector. Базы данных, Redis, OTLP receivers и internal service ports не публикуются. Cross-host calls идут через private network, точные SG и TLS. @@ -453,7 +456,8 @@ Local OTEL queue на каждой VM использует отдельный pe |---|---|---|---| | Веб-домен (frontend) | только `301`/`308` → HTTPS | HTTPS, бизнес-логика | MVP: `tohin.ru`; staging/dev может использовать отдельный host | | API-домен (если выделен) | **не слушает** | только HTTPS | Post-MVP: `api.example.ru` | -| Bitrix callbacks (`/bitrix/*`, `/bitrix/sync/*`) | не обслуживает API; только redirect на том же host | HTTPS | webhook и install URL | +| Bitrix Local App ВМ1 (`/bitrix/handler|install|placement`) | только redirect на web host | HTTPS | install/handler/placement | +| CRM webhook ВМ2 (exact `/bitrix/sync/webhook/contact|alert`) | generic `404/426`, без redirect query token | HTTPS | отдельный processing host | Правила: @@ -494,7 +498,7 @@ Local OTEL queue на каждой VM использует отдельный pe - **веб-домен** (MVP: `tohin.ru`): `/api/*` (REST + WS realtime), `/auth/*`, web frontend; `:80` → redirect HTTPS; `:443` — TLS + маршрутизация; - **выделенный API-домен** (post-MVP, опционально): отдельный `server { listen 443 ssl; ... }` **без** `listen 80`; только `/api/*`; - для `location` WebSocket (`/api/v1/realtime`): `proxy_http_version 1.1`, `Upgrade`/`Connection` headers, увеличенный `proxy_read_timeout`; -- домен или path `/bitrix/*` → `bitrix-local-app`; `/bitrix/sync/*` → `bitrix-sync`; +- только `/bitrix/handler`, `/bitrix/install`, `/bitrix/placement` на ВМ1 → `bitrix-local-app`; CRM `/bitrix/sync/*` на этом host не маршрутизируется; - `GET/POST /bitrix/handler` и `GET/POST /bitrix/install` доступны публично для Bitrix24; - `/bitrix/placement` доступен публично как заглушка UI настроек коннектора; - `/health/live` и `/health/ready` для `bitrix-local-app` доступны только там, где это нужно для healthcheck и проверки Bitrix form URL; diff --git a/architectory/arch-04-settings-and-content.md b/architectory/arch-04-settings-and-content.md index e106829..354079a 100644 --- a/architectory/arch-04-settings-and-content.md +++ b/architectory/arch-04-settings-and-content.md @@ -14,7 +14,7 @@ | **Настройки SMS runtime** | таблица **`sms.sms_setting`** | sender default, provider timeouts, callback flag, worker intervals | | **Контент** | `text_resources`, `popular_questions` | тексты UI | -Managed PostgreSQL **поднимается до** развёртывания приложения. Бизнес-настройки **не дублируются** в `.env`: seed в `app_settings` выполняется миграцией/скриптом модуля `database` **до** первого запуска `api-backend`. +Managed PostgreSQL **поднимается до** развёртывания приложения. Бизнес-настройки **не дублируются** в `.env`: seed в `app_settings` выполняется миграцией/скриптом `module-01-api-backend` **до** первого запуска `api-backend`. ## Источники настроек @@ -74,7 +74,7 @@ Managed PostgreSQL **поднимается до** развёртывания п ## Требования к таблице `app_settings` -Схема: **`han_app`**. Детальная DDL — модуль `database`; arch фиксирует контракт. +Схема: **`han_app`**. Детальная DDL — `module-01-api-backend` и migration owner этой схемы; arch фиксирует контракт. ### Колонки (минимум) @@ -314,6 +314,9 @@ IDGTL_SMS_CALLBACK_PUBLIC_URL=https://tohin.ru/callbacks/idgtl/sms BITRIX_LOCAL_APP_BASE_URL=http://bitrix-local-app:8080 BITRIX_API_INBOX_PATH=/internal/openlines/v1/inbox MESSAGE_SAFETY_URL=https://processing.internal:8443 +# Host-level Compose bind source; не передаётся приложению как runtime path. +MESSAGE_SAFETY_CA_HOST_PATH=/opt/han-chat/secrets/processing-internal-ca.crt +# Путь того же bind внутри контейнера api-backend. MESSAGE_SAFETY_CA_FILE=/run/han-chat/secrets/processing-internal-ca.crt MESSAGE_SAFETY_API_PREFIX=/internal/safety/v2 MESSAGE_SAFETY_CIRCUIT_FAILURE_THRESHOLD=5 @@ -451,7 +454,7 @@ Feature flag, edge allow-list и readiness образуют единый fail-cl Versioned hot settings содержат batch size/wait, claim size, lease TTL, portal limiter refill/burst, max in-flight, retry base/max/horizon, Contact/alert reconciliation intervals, пороги всплеска Contact, восстановленных без webhook, и IDs/стадии/поля/SLA smart process. Новая версия активируется только после полной type/range/cross-field validation; невалидная версия не заменяет последнюю рабочую. -Secrets, DSN, portal host/member ID, inbound source IP CIDR allow-list, custom Contact field names и cutover watermark не являются hot settings. Полный каталог и defaults — [`../modules/module-07-bitrix-sync.md`](../modules/module-07-bitrix-sync.md), §12. +Secrets, DSN, portal host/member ID, inbound source IP CIDR allow-list, custom Contact field names и cutover watermark не являются hot settings. Полный каталог и defaults — [`module-07-bitrix-sync.md`](../VM2_services/documentation/module-07-bitrix-sync.md), §12. ## Keycloak settings bridge для OTP diff --git a/architectory/arch-05-agent-development-process.md b/architectory/arch-05-agent-development-process.md index 1310b53..56eb6a2 100644 --- a/architectory/arch-05-agent-development-process.md +++ b/architectory/arch-05-agent-development-process.md @@ -42,7 +42,7 @@ - Все сервисы при чтении бизнес-данных по умолчанию запрашивают только `record_status = 'A'`. - Исключения допускаются только для аудита, админки, технического восстановления и миграций. - Прикладные сущности, имеют `id`, `created_at`, `updated_at`, `updater_user_id`. -- Системные таблицы (`app_settings`, `text_resources`, `popular_questions`, `sync_queue`, audit, справочники) могут использовать `user_id = NULL` или отдельное поле `actor_type` — по спецификации модуля `database`. +- Системные таблицы (`app_settings`, `text_resources`, `popular_questions`, `sync_queue`, audit, справочники) могут использовать `user_id = NULL` или отдельное поле `actor_type` — по спецификации `module-01-api-backend` и migrations схемы `han_app`. - Для **строковых enum из arch-00** (`Dialog.status`, `Message.safety_status`, `Message.delivery_status`, `sender_type`, `scan_status` и т.п.) справочник sequence **не** обязателен: значения фиксированы контрактом API. - Для больших/изменяемых списков (типы документов post-MVP, причины, классификаторы UI) — справочники с ID (sequence) и расшифровкой. - Для часто используемых фильтров добавляются индексы. @@ -67,7 +67,7 @@ Каждый модуль должен: -- использовать общий формат JSON-логов; +- использовать общий формат JSON-логов из [`arch-07-observability.md`](arch-07-observability.md); - добавлять `module`, `event`, `request_id`, `trace_id`; - для `api-backend` добавлять **`ux_session_id`** в JSON-логи, если передан заголовок `X-Ux-Session-Id`; - не логировать access token, refresh token, raw OTP, документы, полные PII; diff --git a/modules/module-09-observability.md b/architectory/arch-07-observability.md similarity index 70% rename from modules/module-09-observability.md rename to architectory/arch-07-observability.md index fe6be17..7bd937d 100644 --- a/modules/module-09-observability.md +++ b/architectory/arch-07-observability.md @@ -1,7 +1,14 @@ -# module-09. Наблюдаемость production-like контура +# arch-07. Контракт наблюдаемости -> Статус: целевая спецификация наблюдаемости MVP для ВМ1/ВМ2 и отдельного private SigNoz. -> Источники: [`README.md`](README.md), [`arch-00-glossary.md`](arch-00-glossary.md), [`arch-01-system-architecture.md`](arch-01-system-architecture.md), [`arch-02-api-contracts.md`](arch-02-api-contracts.md), [`arch-03-docker-compose-blueprint.md`](arch-03-docker-compose-blueprint.md), [`arch-04-settings-and-content.md`](arch-04-settings-and-content.md), [`arch-05-agent-development-process.md`](arch-05-agent-development-process.md), [`module-01-api-backend.md`](module-01-api-backend.md)–[`module-08-keycloak.md`](module-08-keycloak.md). +> Канонический контракт telemetry для всех application VM и private SigNoz. +> Реализация на конкретной VM — в [`module-09-observability-vm1.md`](../VM1_app/documentation/module-09-observability-vm1.md) и [`module-09-observability-vm2.md`](../VM2_services/documentation/module-09-observability-vm2.md). +> Имена сущностей — [`arch-00-glossary.md`](arch-00-glossary.md). Границы системы — [`arch-01-system-architecture.md`](arch-01-system-architecture.md). HTTP — [`arch-02-api-contracts.md`](arch-02-api-contracts.md). Compose — [`arch-03-docker-compose-blueprint.md`](arch-03-docker-compose-blueprint.md). Env — [`arch-04-settings-and-content.md`](arch-04-settings-and-content.md). Процесс — [`arch-05-agent-development-process.md`](arch-05-agent-development-process.md). Host security — [`arch-06-service-hosting-security.md`](arch-06-service-hosting-security.md). + +## Назначение + +Документ фиксирует то, что **должно совпасть между ВМ1 и ВМ2**: схема сигналов, корреляция, redaction, sampling, Collector pipeline и удаленный backend. Локальные `service.name`, scrape targets, дашборды и алерты конкретной машины в этом файле не детализируются. + +Этот контракт **нельзя** независимо кастомизировать в репозитории VM. Изменение JSON-полей, labels, redaction, sampling или `service.namespace` сначала вносится сюда. ## 1. Цели и границы @@ -22,27 +29,24 @@ Telemetry не является источником бизнес-истины Каждый root Compose (ВМ1 и ВМ2) обязан содержать локальный `otel-collector`. Prometheus/Grafana/Loki/Tempo не обязаны размещаться на application VM. -Предпочтительный operable-вариант после выбора backend: +Операбельный вариант: 1. приложения на каждой VM экспортируют OTLP gRPC только в свой local `otel-collector:4317`; -2. Collector отправляет telemetry в выбранный удалённый управляемый OTLP backend провайдера; +2. Collector отправляет telemetry в self-hosted SigNoz на отдельной VM `192.168.0.5:4317`; 3. JSON stdout остаётся аварийным локальным журналом Docker с rotation; -4. пока удалённый backend не выбран, допустим архитектурный минимум из arch-03: bounded JSON stdout/platform logs и Collector `debug` exporter с sampling в acceptance; такой режим не считается полноценным production-хранением и не закрывает alerting/SLO. +4. пока удалённый backend недоступен в конкретном окружении, допустим архитектурный минимум из arch-03: bounded JSON stdout/platform logs и Collector `debug` exporter с sampling в acceptance; такой режим не считается полноценным production-хранением и не закрывает alerting/SLO. -Требуемые возможности удалённого backend: OTLP ingest, поиск traces, PromQL-совместимые или эквивалентные metrics, поиск структурированных logs, alerting, RBAC, retention и TLS. +Требуемые возможности backend: OTLP ingest, поиск traces, PromQL-совместимые или эквивалентные metrics, поиск структурированных logs, alerting, RBAC, retention и TLS. Для текущего SigNoz plaintext OTLP разрешён только внутри доверенной приватной сети; UI — через SSH jump host. + +ВМ2 никогда не использует Docker hostname collector ВМ1. Каждый collector имеет собственный `otel-queue`. ### 2.2. Самостоятельно размещаемая опция -Опциональный Compose profile `observability-local` может включать: +Опциональный Compose profile `observability-local` может включать Prometheus, Grafana, Loki и Tempo. Он **не включается по умолчанию на малой VM**: стек требует дополнительной RAM/диска и сам становится объектом backup/monitoring. -- Prometheus — scrape метрик Collector/Redis/Keycloak/nginx exporters; -- Grafana — dashboards и alerts; -- Loki — логи; -- Tempo — traces. +Для operable local-варианта нужны отдельный volume каждому backend, retention limits, compaction, auth через ops/VPN и отсутствие host ports. Grafana доступна только через отдельный защищённый ops route/VPN, не через публичный `/`. -Он **не включается по умолчанию на малой VM**: стек требует дополнительной RAM/диска и сам становится объектом backup/monitoring. Для operable local-варианта нужны отдельный volume каждому backend, retention limits, compaction, auth через ops/VPN и отсутствие host ports. Grafana доступна только через отдельный защищённый ops route/VPN, не через публичный `/`. - -Рекомендуемый минимум VM при local profile: дополнительно 4 vCPU, 8 ГБ RAM и 100+ ГБ SSD сверх приложения; точный размер — после измерения ingest. +Рекомендуемый минимум VM при local profile: дополнительно 4 vCPU, 8 ГБ RAM и 100+ ГБ SSD сверх приложения; точный размер — после измерения ingest. Нужен ли profile на ВМ1 — TBD профильной спецификации ВМ1. ## 3. Архитектура Collector @@ -51,7 +55,7 @@ Telemetry не является источником бизнес-истины ```text backend services ─OTLP gRPC/HTTP─┐ nginx/Redis/Keycloak exporters ──┼─> otel-collector -Docker JSON stdout ─filelog───────┘ ├─ OTLP/TLS remote backend +Docker JSON stdout ─filelog───────┘ ├─ OTLP remote backend (SigNoz) ├─ Prometheus endpoint (optional) └─ debug exporter (acceptance only) ``` @@ -62,7 +66,7 @@ Collector запускается одним сервисом MVP. При рос - `otlp` gRPC `0.0.0.0:4317` — основной internal receiver; - `otlp` HTTP `0.0.0.0:4318` — совместимость SDK; -- `prometheus` — scrape самого Collector, Keycloak metrics, Redis exporter, nginx exporter и сервисных `/metrics`, если они не идут OTLP; +- `prometheus` — scrape самого Collector, exporters и сервисных `/metrics`, если они не идут OTLP; - `filelog` — только если Docker logging driver предоставляет read-only каталог/volume; парсит JSON stdout без чтения secret-файлов; - `hostmetrics` — CPU, memory, filesystem, network VM/container host, если Collector получает только необходимые read-only mounts. @@ -85,10 +89,10 @@ Collector запускается одним сервисом MVP. При рос ### 3.4. Exporters -- `otlp/remote`: TLS verify, endpoint и auth header из secret env/mount; +- `otlp/remote`: endpoint из secret env/mount; для SigNoz в доверенной private network — plaintext `192.168.0.5:4317`; для любого иного remote — TLS verify; - `prometheus`: optional pull endpoint только internal; - `debug`: только `APP_ENV=test|acceptance`, verbosity normal; production debug exporter по умолчанию выключен; -- `loki`/`otlphttp` — только если выбран backend и его контракт закреплён. +- `loki`/`otlphttp` — только если выбран дополнительный backend и его контракт закреплён. Секрет exporter-а не должен появляться в rendered config, логах или `/debug/configz`. Config монтируется read-only; secret подставляется env. @@ -140,7 +144,9 @@ processors: exporters: otlp/remote: endpoint: "${env:OTEL_REMOTE_ENDPOINT}" - tls: {insecure: false} + # true только для утверждённого plaintext SigNoz внутри private network; + # для любого другого remote — false и проверка CA. + tls: {insecure: ${env:OTEL_REMOTE_TLS_INSECURE}} headers: {authorization: "${env:OTEL_REMOTE_AUTH_HEADER}"} extensions: @@ -166,18 +172,35 @@ service: metrics: {address: 0.0.0.0:8888} ``` -Конкретная версия schema проверяется командой Collector `validate`; image закрепляется по digest. Значения memory/batch/queue — стартовые, не SLO. +Конкретная версия schema проверяется командой Collector `validate`; image закрепляется по digest. Значения memory/batch/queue — стартовые, не SLO. Для текущего SigNoz `tls.insecure` и auth header задаются профильной спецификацией VM согласно O1/O-TBD1. Scrape targets кроме самого Collector добавляет спецификация VM. ## 4. Resource attributes и корреляция Обязательные resource attributes: -- `service.name`: `nginx`, `api-backend`, `message-safety`, `bitrix-local-app`, `bitrix-sync`, `keycloak`, `redis`, `otel-collector`; +- `service.name` — только из реестра ниже; - `service.namespace=han-chat`; - `service.version=`; - `deployment.environment=production-like|production`; - `host.name`/`service.instance.id` без публичного IP. +Реестр `service.name`: + +| Имя | Владелец | +|---|---| +| `nginx` | nginx каждой VM; различать экземпляры `host.name` / `service.instance.id` | +| `api-backend` | ВМ1 | +| `bitrix-local-app` | ВМ1 | +| `keycloak` | ВМ1 | +| `sms-service` | ВМ1, callback/internal API | +| `sms-worker` | ВМ1, отправка в Direct | +| `message-safety` | ВМ2 | +| `bitrix-sync` | ВМ2 | +| `redis` | Redis каждой VM; различать экземпляры | +| `otel-collector` | collector каждой VM | + +Новое имя добавляется только сюда, затем в спецификацию владельца. + Обязательные поля request-события: - `request_id`; @@ -191,8 +214,8 @@ service: ## 5. W3C propagation - принимаются только валидные `traceparent` и опциональный `tracestate`; -- nginx передаёт context в API; при edge instrumentation создаёт server span; -- API создаёт child spans для PostgreSQL, Redis, S3, Safety, Open Lines и JWKS; +- nginx передаёт context upstream; при edge instrumentation создаёт server span; +- caller создаёт child spans для своих зависимостей (PostgreSQL, Redis, S3, Safety, Open Lines, JWKS, CRM); - internal calls передают `traceparent`, `tracestate`, `X-Request-ID`; - `baggage` по умолчанию не принимается от внешнего клиента; если включён, allow-list исключает PII; - Bitrix24/S3 могут не вернуть context: внешний client span всё равно закрывается результатом; @@ -252,9 +275,11 @@ Frontend может отправлять валидный `traceparent`, но ba Redaction выполняется в SDK/logger **до stdout**, затем повторяется Collector processor. Collector не может считаться единственной защитой. Автотесты отправляют canary secrets/PII и требуют отсутствие во всех трёх сигналах. -## 8. Instrumentation по компонентам +## 8. Общие правила instrumentation -### 8.1. FastAPI-сервисы +Детальный список инструментов конкретной VM — в профильной спецификации. Ниже правила, общие для всех сервисов. + +### 8.1. FastAPI и workers - OpenTelemetry ASGI/FastAPI server spans с route template; - HTTPX client spans с sanitized host/method/status; @@ -272,11 +297,7 @@ Redaction выполняется в SDK/logger **до stdout**, затем по `redis_exporter` подключается отдельным ACL user только на `INFO`, `PING`, безопасные latency/keyspace metrics. Нужны memory ratio, evictions, expirations, blocked/rejected clients, command latency, AOF status/rewrite, Pub/Sub buffers, key count/TTL агрегаты. Keys/values не экспортируются. -### 8.4. Keycloak - -Включаются management metrics/JVM/HTTP/DB pool. Custom OTP provider публикует counters send/verify/limit/settings-cache без phone labels. Login events идут в JSON/audit с masked/HMAC destination. Public OIDC synthetic проверяется отдельно. - -### 8.5. nginx +### 8.4. nginx JSON access log содержит `request_id`, извлечённый `trace_id`, route class, method, normalized path, status, bytes, request/upstream duration/status, TLS version, cache status. `$request` с query не используется. @@ -289,74 +310,29 @@ Collector `filelog` parser: - отбрасывает ACME/health success noise; - не парсит raw URI в labels. -Native nginx OTEL module предпочтителен, если image/version закреплены. Без него nginx только передаёт W3C context и коррелирует access log; первый server span создаёт API. +Native nginx OTEL module предпочтителен, если image/version закреплены. Без него nginx только передаёт W3C context и коррелирует access log; первый server span создаёт upstream. -### 8.6. Host/Docker +### 8.5. Host/Docker CPU, load, memory/swap, disk usage/inodes/IO, network, container restarts/OOM, Docker daemon health и clock sync. Container name/version — bounded labels; container id не хранится как долгосрочный high-cardinality label. -## 9. Метрики бизнес-потоков +## 9. Метрики: запрет high-cardinality -### API и auth +Никакие UUID/user/session/dialog/task/message id не labels. Они допустимы только в sampled logs/traces при принятой retention. Allow-list labels; route template вместо raw path; status class/known code; dependency enum. -- `han_http_requests_total{service,route,method,status_class}`; -- `han_http_request_duration_seconds`; -- `han_auth_bootstrap_total{outcome}`; -- `han_ux_session_start_total{reason}`; -- `han_jwks_refresh_total{outcome}`; -- `han_rate_limit_decisions_total{scope,outcome}`. +Карта бизнес-метрик принадлежит спецификации VM-владельца сервиса. Сквозные SLI ниже используют сигналы обеих VM. -### Message Safety +## 10. Сквозные dashboards в SigNoz -- checks/verdicts по `allow|deny|pending|error`; -- `processing_mode`, active/used `config_version`, config activation result, `message_safety_mock_enabled` и forced outcomes по `text|file`/`allow|deny`; -- poll duration/count buckets, timeout и recovery backlog age; -- stub mode info и terminal `400` отдельно, пока действует test-only контракт; -- cache hit, Redis latency, task expired/not-found. - -### Open Lines/Bitrix - -- message submitted → delivered end-to-end latency; -- local app outbound result/retry/ambiguous/DLQ; -- inbox depth/oldest age/forward retries/duplicate; -- OAuth time-to-expiry/refresh result; -- connector desired/observed state; -- Bitrix 429, circuit state, setup failure. - -### Files/S3 - -- init/complete/promote/delete; -- quarantine object age/orphans; -- checksum/MIME/size reject; -- presigned download issued; -- S3 dependency latency/error by operation and logical bucket. - -### Frontend synthetic - -- public config/content; -- OIDC discovery/authorization page; -- WS handshake; -- test-user end-to-end flow в отдельной тестовой identity без реального PII. - -Никакие UUID/user/session/dialog/task/message id не labels. Они допустимы только в sampled logs/traces при принятой retention. - -## 10. Dashboards +SigNoz обязан иметь: 1. **Executive/SLO**: availability, error budget burn, p50/p95/p99, message delivery, auth, active incidents. -2. **nginx ingress по VM**: отдельные панели/filters ВМ1 и ВМ2; RPS, 4xx/5xx, upstream latency/status, 429, WS, TLS, cache, CRM webhook на ВМ2. -3. **api-backend**: routes, DB/Redis pools, JWKS, circuits, outbox/safety backlog, S3. -4. **message-safety**: capabilities, verdicts, `202` poll, PG queue age/leases/fencing, ClamAV/signature age, file/link cache и DNS dependency. -5. **bitrix-local-app**: install/OAuth, connector, outbound/inbox/DLQ, API/Bitrix latency. -6. **bitrix-sync**: mode/readiness, queue depth/oldest age, workflow/command transitions, CRM batch latency/subcommand outcome, limiter/throttle, retry/DLQ, webhook/reconciliation lag, mapping invariants и business-alert SLA. -7. **Keycloak**: login/OTP/lockout, sessions/tokens, provider settings cache, JVM/DB. -8. **Redis**: memory/evictions/AOF/latency/clients/keyspace. -9. **PostgreSQL/S3**: provider metrics, storage, connections, backup/PITR, object errors. -10. **Business flow**: guest config → OTP → bootstrap → session → dialog → safety → Open Lines → operator reply. -11. **Collector health**: accepted/sent/refused/dropped, queue, retry, exporter errors, memory/CPU. +2. **Business flow**: guest config → OTP → bootstrap → session → dialog → safety → Open Lines → operator reply. +3. **Collector health**: accepted/sent/refused/dropped, queue, retry, exporter errors, memory/CPU — отдельно по `host.name` ВМ1 и ВМ2. -Каждая панель содержит release annotation, environment filter и links trace→logs по `trace_id`. +Сервисные дашборды nginx/API/Safety/Keycloak/Redis/sync — в спецификациях VM. Каждая панель содержит release annotation, environment filter и links trace→logs по `trace_id`. -## 11. SLI, SLO и alerts +## 11. SLI, SLO и общие alerts Значения — начальная production-like политика до load/product review: @@ -374,34 +350,14 @@ CPU, load, memory/swap, disk usage/inodes/IO, network, container restarts/OOM, D Файловый send измеряется отдельно: p95 не должен превышать configured safety poll budget; user-cancel, safety deny, 4xx validation и edge abuse 429 не считаются server failure. 503/504 и unexpected 5xx считаются. -### Paging alerts +Paging/ticket alerts, привязанные к конкретному сервису, задаёт спецификация VM. Общие инфраструктурные paging alerts: - multi-window burn: 14.4× за 5m/1h или 6× за 30m/6h; -- edge/API 5xx >5% 5 минут; -- text delivery failure >5% 10 минут; -- oldest outbox/inbox/safety task >5 минут либо DLQ >0; -- Keycloak login failures infrastructure class >10% 5 минут; - PostgreSQL unavailable/connection saturation >90%; -- Redis unavailable, AOF error или sustained evictions; - Collector exporter queue >80%, dropped/refused telemetry >0 sustained; - TLS expiry <14 дней warning, <7 дней page; - disk >85% warning, >92% page; OOM/restart loop; - managed PG backup/PITR failure. -- Message Safety active config missing/invalid, referenced artifact unavailable или config refresh stale >5 с. -- `message_safety_mock_enabled=1` — active page/high-severity alert без auto-resolve по времени; закрывается только после возврата в standard. -- bitrix-sync invalid/revoked credential или обязательная configuration/grant missing; -- bitrix-sync technical DLQ >0, mapping invariant violation или worker/limiter heartbeat stale; -- bitrix-sync queue oldest age >30 с при healthy CRM либо sustained рост; -- Contact webhook/reconciliation cursor lag выше двух configured intervals. - -### Ticket/warning alerts - -- p95 regression 20% release-over-release; -- settings/JWKS cache stale; -- bitrix-sync CRM last success stale, rate-limit errors выше baseline или business alert SLA overdue; -- OAuth expires <24h без успешного refresh; -- quarantine orphan growth; -- cardinality/ingest growth >2× baseline. Alert содержит service, environment, symptom, dashboard, runbook, release и безопасный query; не содержит PII. @@ -417,7 +373,7 @@ Alert содержит service, environment, symptom, dashboard, runbook, releas ### Metrics -Allow-list labels; route template вместо raw path; status class/known code; dependency enum. Cardinality budget: целевой <10 000 active series на MVP environment. CI проверяет запрещённые labels. +Cardinality budget: целевой <10 000 active series на MVP environment. CI проверяет запрещённые labels. ### Retention initial @@ -443,9 +399,9 @@ Legal retention/erasure имеет приоритет; изменение тре - scrape failures; - config reload/validation. -При remote outage queue хранится на `otel-queue` с bounded size/age. При заполнении отбрасываются сначала low-priority success traces/logs; приложение продолжает работу. Нельзя позволять queue заполнить системный диск. +При remote outage queue хранится на `otel-queue` с bounded size/age. При заполнении отбрасываются сначала low-priority success traces/logs; приложение продолжает работу. Нельзя позволять queue заполнить системный диск. Telemetry outage/overflow fail-open для business и Safety readiness, но создаёт alert. -## 14. Docker Compose +## 14. Docker Compose collector `otel-collector`: @@ -464,8 +420,6 @@ Legal retention/erasure имеет приоритет; изменение тре - restart policy с backoff; - приложения имеют bounded non-blocking OTLP exporter queue. -Collector существует отдельным экземпляром на ВМ1 и ВМ2. Каждый имеет собственный `otel-queue` volume/limits и экспортирует в private SigNoz `192.168.0.5:4317`; ВМ2 никогда не использует Docker hostname collector ВМ1. Telemetry outage/overflow fail-open для business и Safety readiness, но создаёт alert. - Новый named volume считается потенциально `root:root`; основной collector не запускается от root и volume не получает `0777`. Ownership init проверяется после первого create и повторного recreate. @@ -474,8 +428,8 @@ Collector существует отдельным экземпляром на В ## 15. Security -- OTLP receiver internal-only; при переходе между hosts — mTLS; -- remote exporter только TLS verify, credentials least privilege; +- OTLP receiver internal-only; при переходе между hosts — mTLS, кроме явно зафиксированного plaintext SigNoz в private network; +- remote exporter credentials least privilege; - Grafana/Prometheus/Loki/Tempo не публичны; - RBAC: viewer/operator/admin; audit доступа к logs/traces; - dashboards не показывают PII; @@ -485,7 +439,7 @@ Collector существует отдельным экземпляром на В - telemetry input не исполняет expressions из пользовательских значений; - регулярная secret-canary проверка и incident deletion procedure. -## 16. Runbooks +## 16. Общие runbooks ### Collector not-ready @@ -520,14 +474,6 @@ Collector существует отдельным экземпляром на В 4. Rollback instrumentation при риске стоимости/доступности. 5. Добавить CI regression test. -### Высокая latency сообщения - -1. Открыть trace по request id. -2. Разделить API, Safety poll, S3, local app, Bitrix. -3. Проверить circuit, queue age, DB pool и Redis. -4. Не повторять ambiguous message без исходного idempotency key. -5. Следовать runbook зависимого модуля. - ### Логи содержат секрет/PII 1. Ограничить доступ и остановить offending export. @@ -536,58 +482,58 @@ Collector существует отдельным экземпляром на В 4. Удалить данные по процедуре backend/provider. 5. Исправить source redaction + Collector defense; добавить canary test. -## 17. Проверки и Definition of Done +Разбор высокой latency сообщения — в спецификации VM, которая владеет соответствующим hop. + +## 17. Общий Definition of Done - Collector config проходит validate и запускается в root Compose каждой VM; - OTLP gRPC и HTTP принимают три сигнала; -- все сервисы имеют правильные resource attributes; +- все сервисы имеют правильные resource attributes из реестра §4; - request проходит nginx/API/Safety/Open Lines с одним `request_id` и связанным trace; - `ux_session_id` есть только где передан и не является label; -- FastAPI/HTTPX/PG/Redis/S3 workers instrumented; -- nginx JSON parsing и trace correlation проверены; -- Redis/Keycloak/host/Collector metrics доступны; -- dashboards и alerts provisioned из versioned files для выбранного telemetry backend; до его выбора это остаётся acceptance/TBD, а не выполненный production DoD; - remote outage, queue full, Collector restart и backend recovery rehearsed; - local profile, если включён, имеет volumes/retention/auth и не публикует порты; - secret/PII canary отсутствует в logs/traces/metrics; - cardinality и sampling tests проходят; - SLO queries воспроизводимы и исключения документированы; -- runbooks связаны с alerts. +- runbooks связаны с alerts; +- dashboards и alerts provisioned из versioned files для SigNoz; до полного provisioning это остаётся acceptance/TBD, а не выполненный production DoD. + +Профильный DoD VM дополняет этот список своими сервисами и не переопределяет контракт. ## 18. Допущения, TBD и конфликты ### Решения -- O1: обязательный архитектурный минимум — local Collector на каждой application VM; выбран self-hosted SigNoz - на отдельной VM `192.168.0.5`, доступный по приватному OTLP gRPC. +- O1: обязательный архитектурный минимум — local Collector на каждой application VM; выбран self-hosted SigNoz на отдельной VM `192.168.0.5`, доступный по приватному OTLP gRPC. - O2: Prometheus/Grafana/Loki/Tempo — отдельный operable profile, не скрытая обязательная нагрузка основной VM. - O3: JSON stdout — аварийный локальный buffer; audit App DB — durable. - O4: telemetry fail-open для business path, но потеря telemetry alertится. - O5: ID/PII не labels; source redaction обязательна до Collector. +Поля JSON, resource attributes, redaction, sampling, `service.namespace` и адрес SigNoz меняются только здесь. + ### TBD -- O-TBD1 закрыт: self-hosted SigNoz, `192.168.0.5:4317`, plaintext только - внутри доверенной приватной сети; UI через SSH jump host. +- O-TBD1 закрыт: self-hosted SigNoz, `192.168.0.5:4317`, plaintext только внутри доверенной приватной сети; UI через SSH jump host. - O-TBD2: утвердить SLO/RPS/error-budget с product owner. - O-TBD3: legal retention/erasure и допустимость IP/user-agent. - O-TBD4: точные sampling и resource limits после load test. - O-TBD5: поддерживаемый nginx OTEL module и Keycloak native tracing по pinned versions. -- O-TBD6: нужен ли local observability profile в первой VM. +- O-TBD6: нужен ли local observability profile в первой VM — решает спецификация ВМ1. ### Обнаруженные архитектурные конфликты -1. `arch-03` разрешает stdout/platform exporter как минимум, но production-like расследования и alerts без backend ограничены. Здесь remote OTLP backend рекомендован, но не объявлен выбранным: провайдер остаётся TBD. -2. `module-05` использует test-only terminal `400` и non-sticky verdict вместо canonical `403`/sticky production verdict. Dashboards обязаны маркировать сервис `stub`; production SLO Safety на нём недостоверен. -3. `module-07` задаёт full sync target; до code/portal cutover dashboard обязан показывать `sync_disabled`, а не синтетический CRM success. Queue/CRM SLI включаются только после module-07 preflight. +1. `arch-03` разрешает stdout/platform exporter как минимум, но production-like расследования и alerts без backend ограничены. Закрыто выбором SigNoz (O1 / O-TBD1); stdout остаётся аварийным buffer. +2. `module-05` использует test-only terminal `400` и non-sticky verdict вместо canonical `403`/sticky production verdict. Dashboards обязаны маркировать сервис `stub`; production SLO Safety на нём недостоверен. Детали — спецификация ВМ2. +3. `module-07` задаёт full sync target; до code/portal cutover dashboard обязан показывать `sync_disabled`, а не синтетический CRM success. Queue/CRM SLI включаются только после module-07 preflight. Детали — спецификация ВМ2. 4. Retention, RPO/RTO и production SLO открыты в module-01/04/06/08; значения этого документа являются initial ops policy, не закрывают legal/product TBD. -5. Observability env (`OTEL_REMOTE_*`, service names, sampling/queue limits) - добавлены в arch-04 и `.env.example`; sampling/queue limits уточняются после - load test. +5. Observability env (`OTEL_REMOTE_*`, service names, sampling/queue limits) добавлены в arch-04 и `.env.example`; sampling/queue limits уточняются после load test. -## 19. Ссылки на прототип и исходные документы +## 19. Ссылки +- Профильные спецификации: [`module-09-observability-vm1.md`](../VM1_app/documentation/module-09-observability-vm1.md), [`module-09-observability-vm2.md`](../VM2_services/documentation/module-09-observability-vm2.md). - VM/Docker logging и firewall: [`../../HAN_chat/deploy/setup-vm-han-chat.sh`](../../HAN_chat/deploy/setup-vm-han-chat.sh). - Прототипный nginx stdout/access log: [`../../HAN_chat/bitrix-local-app/deploy/nginx/nginx.conf`](../../HAN_chat/bitrix-local-app/deploy/nginx/nginx.conf). -- Архитектурный observability contract: [`arch-02-api-contracts.md`](arch-02-api-contracts.md). +- HTTP/OTLP registry: [`arch-02-api-contracts.md`](arch-02-api-contracts.md). - Compose topology: [`arch-03-docker-compose-blueprint.md`](arch-03-docker-compose-blueprint.md). diff --git a/architectory/arch-08-nginx.md b/architectory/arch-08-nginx.md new file mode 100644 index 0000000..b3734cd --- /dev/null +++ b/architectory/arch-08-nginx.md @@ -0,0 +1,233 @@ +# arch-08. Контракт корневого nginx + +> Канонический контракт nginx для всех application VM. +> Compose-топология, сети и published ports — [`arch-03-docker-compose-blueprint.md`](arch-03-docker-compose-blueprint.md). +> Реализация на конкретной VM — [`module-03-nginx-vm1.md`](../VM1_app/documentation/module-03-nginx-vm1.md) и [`module-03-nginx-vm2.md`](../VM2_services/documentation/module-03-nginx-vm2.md). +> Telemetry access log — [`arch-07-observability.md`](arch-07-observability.md). Env — [`arch-04-settings-and-content.md`](arch-04-settings-and-content.md). Host security — [`arch-06-service-hosting-security.md`](arch-06-service-hosting-security.md). + +## Назначение + +Документ фиксирует то, что **должно совпасть между ВМ1 и ВМ2**: независимый nginx на каждой машине, TLS/ACME, request id, forwarded headers, запрет internal paths, JSON access log, hardening Compose и failure/reload policy. + +Routing matrix, upstreams, SPA, WebSocket, CSP и allow-list конкретной машины здесь не детализируются и **не кастомизируются** так, чтобы сломать этот контракт. + +## 1. Обязательная топология + +В production-like контуре каждая VM имеет собственный nginx в своём root Compose и собственный deployment lifecycle. + +- публичный трафик одной VM не проходит через nginx другой; +- отказ или deploy одной VM не обязан прерывать ingress другой; +- контейнеры приложений не публикуют host ports; на каждой VM наружу смотрит только её nginx; +- если перед конкретной VM есть WAF/LB, trusted proxy CIDR задаются отдельно (N1). + +Между public route ВМ1 и ВМ2 нет reverse-proxy chain и нет SPA/API fallback на другую VM. + +## 2. Общие правила routing + +Порядок location критичен. Exact locations объявляются до prefix. + +На **обоих** public hosts: + +- `/internal/`, `/_internal/`, Redis/OTLP/admin/status/config files возвращают `404`; +- fallback на другую VM или чужой SPA запрещён; +- query не участвует в exact location matching; +- адрес из недоверенного `X-Forwarded-For` не используется для allow-list; +- при внешнем LB сначала настраиваются его trusted CIDR и нормализация real IP. + +Upstream failures не перенаправляются на другой сервис и не попадают в SPA. API-подобные locations возвращают `502/504` с безопасным nginx body и `X-Request-ID`; custom JSON error допустим только там, где профильная спецификация это разрешает, и не имитирует backend domain code. + +Проверка prompt injection / malware не выполняется в nginx: это `message-safety` через `api-backend` (arch-02). + +## 3. HTTP/HTTPS и TLS + +- public host каждой VM на `:80` обслуживает только ACME challenge и `308 https://$host$request_uri`, кроме явно зафиксированных исключений профильной спецификации; +- выделенный API host, если появится, не имеет listener `:80`; +- `:443 ssl http2`, TLS 1.2/1.3, современные cipher suites, session tickets по ops policy; +- сертификат доверенного CA, private key read-only и недоступен приложению; +- OCSP stapling при поддержке CA/DNS; +- HSTS включается только после успешной проверки HTTPS: `max-age` из env, затем по решению ops `includeSubDomains`; preload не включать автоматически; +- OIDC redirects, cookies и external URLs всегда HTTPS; +- `Server` tokens скрыты; upstream `X-Powered-By` удаляется. + +ВМ2 дополнительно слушает private `8443` с сертификатом internal CA — детали в спецификации ВМ2. + +## 4. ACME lifecycle + +Выбран webroot Certbot/ACME client с общими named volumes: + +```text +nginx-certs -> /etc/letsencrypt (rw у certbot, ro у nginx) +nginx-acme-webroot -> /var/www/certbot +``` + +Каждая VM выпускает **свой** сертификат на свой public host. Секреты и volumes двух projects не общие. + +Bootstrap: + +1. DNS указывает на VM; 80/443 разрешены. +2. Запустить временный HTTP config с `/.well-known/acme-challenge/`. +3. Выпустить certificate без остановки nginx. +4. Проверить `nginx -t`, атомарно активировать TLS config, reload. + +Renew container/host timer выполняет `certbot renew` минимум дважды в сутки; +после фактического renewal проверяет рабочую конфигурацию командой +`docker compose exec -T nginx nginx -t -c /tmp/nginx.conf` и отправляет +master-процессу `docker compose kill -s HUP nginx`. Bare-команды `nginx -t` и +`nginx -s reload` запрещены: контейнер read-only, а рабочие config/PID находятся +в `/tmp`. При ошибке остаётся старый worker/config/cert и срабатывает alert. +Успешный deploy/renew hook возвращает `0` с пустым stderr: вывод успешного +`nginx -t` и progress signal command перехватывается или подавляется; при +ошибке сохранённая диагностика полностью печатается в stderr. Любой stderr на +success path считается дефектом интеграции с Certbot. +Контролируются expiry days и последняя успешная попытка. Staging CA используется +в rehearsal, чтобы не исчерпать лимиты. + +Non-root nginx не монтирует root-only дерево Let's Encrypt целиком: только необходимые cert/key files по arch-03/arch-06. + +## 5. Request ID и forwarded headers + +На edge формируется trusted request id. Базовый nginx не генерирует UUID штатной переменной, поэтому используется njs/Lua либо модуль request-id, включённый в закреплённый image. Входящий `X-Request-ID` принимается только если соответствует UUID/ULID и длине; иначе генерируется новый. + +Upstream получает: + +```text +Host: original host +X-Real-IP: trusted real client IP +X-Forwarded-For: normalized proxy chain +X-Forwarded-Proto: https +X-Forwarded-Host: original host +X-Forwarded-Port: 443 +X-Request-ID: edge request id +traceparent: входной валидный либо новый согласно OTEL integration +``` + +Ответ всегда содержит `X-Request-ID`. Клиентские `X-Forwarded-*` от недоверенного адреса перезаписываются. `Authorization`, `Cookie`, query string и body не попадают в access log. + +Private listener доверяет forwarded headers только от allow-listed private caller; public listener применяет собственную trusted proxy policy. + +## 6. Timeouts и body limits — общие ориентиры + +| Группа | connect/send/read | Владелец | +|---|---|---| +| обычный API | 3s / 30s / 30s | ВМ1 | +| auth | 3s / 30s / 60s | ВМ1 | +| Bitrix local callback | 3s / 30s / 60s | ВМ1 | +| Direct SMS callback | 3s / 30s / 60s | ВМ1 | +| WS | 3s / 30s / 75s+ | ВМ1 | +| message POST | 3s / 30s / `MESSAGE_SAFETY_TASK_POLL_MAX_SEC + 30s` минимум | ВМ1 | +| CRM webhook | 3s / 30s / 60s | ВМ2 | +| private Safety | по контракту Safety, не короче caller budget | ВМ2 | + +Значение timeout генерируется из env template до startup; nginx не выполняет арифметику env runtime. + +`client_max_body_size` global 8m по arch-04, но JSON locations получают более строгие limits, где возможно. Байты вложения не проходят через nginx/API: клиент PUT напрямую в S3. Buffering request допустим для малого JSON; для callback устанавливается bounded temp storage. Header count/size ограничены. + +## 7. Edge rate limits — общие правила + +`limit_req_zone` использует binary remote address. Ответ превышения — `429`, `Retry-After` (статический/вычисляемый для зоны) и request id. Edge не реализует user-level бизнес-лимит; это делает API/Redis. + +Карта зон принадлежит спецификации VM. Новые зоны добавляются только там, затем при необходимости сюда как реестр имён. + +## 8. Health + +- внутренний `GET /nginx-health/live` возвращает static 200 и доступен Docker healthcheck; +- внешний health публикуется только если нужен мониторингу, с allow-list (N3); +- nginx health не утверждает готовность upstream; +- Docker healthcheck использует только binary, гарантированно присутствующий + и проверенный внутри exact pinned nginx digest; `wget`/`curl` запрещены, если + их наличие не подтверждено image inventory; +- upstream `/health/ready` не агрегируется публично без решения ops. + +## 9. Логи и OTEL correlation + +JSON access log: timestamp, request_id, trace_id (если извлечён), remote IP/hashed policy, host, method, route class, normalized URI на основе `$uri` без `$request_uri`/`$args`, status, bytes, duration, upstream addr/status/time, cache status, TLS protocol/cipher, user agent при принятой retention. + +Не логируются Authorization, cookies, request/response body, OTP, tokens, query token, presigned query, PII. Error log структурирован настолько, насколько позволяет nginx; debug выключен production. + +Nginx передаёт W3C trace context; native OTEL module допустим при закреплённой версии (arch-07 O-TBD5). Если edge создаёт span, request id остаётся отдельным correlation key. Логи идут stdout/stderr; Docker/collector отвечает за доставку и rotation. + +Parser и `service.name=nginx` — arch-07 §8.4. + +## 10. Layout конфигурации + +Общий каркас репозитория nginx: + +```text +nginx/ + Dockerfile + docker-compose.yml + nginx.conf + templates/ + 00-maps.conf.template + 10-upstreams.conf.template + 20-http-redirect.conf.template + 30-https-site.conf.template + snippets/ + proxy-common.conf + security-headers.conf + tls.conf + rate-limits.conf + njs/request_id.js + scripts/{render,validate,reload-after-renew}.sh + tests/ +``` + +Профильная спецификация добавляет только нужные snippets (`websocket.conf`, private server block, webhook allow-list). Image и modules pin по digest/version. Render использует allow-list env и fail-fast для пустых host/cert/upstream/timeouts. Секреты в rendered config не требуются. + +## 11. Docker Compose + +`nginx` подключён к `public` и `backend` (или эквивалентным сетям своей VM). Публикация host ports разрешена только nginx. Filesystem read-only, tmpfs для cache/run/temp/`/etc/nginx/conf.d` по arch-03, non-root где позволяет bind ports/capabilities. Cert volumes read-only для nginx. ACME client имеет только необходимые volumes/network. + +Non-root nginx получает writable tmpfs только для `/etc/nginx/conf.d`, `/var/cache/nginx`, `/var/run` и `/tmp`; tmpfs задаёт явные UID/GID/mode. Основной `nginx.conf` подключает конкретный rendered include, не неограниченный wildcard, который позволил бы обойти `nginx -t`. + +`depends_on` health не заменяет retry/readiness. После healthy upstream +обязателен config test с production service DNS names и reload/recreate nginx. +После recreate upstream повторяется reload policy либо используется явно +протестированный dynamic resolver. Nginx может временно отдавать bounded 502, +но не считается ready до этой post-ready проверки. + +Published Docker ports сопоставляются original host destination через conntrack/`DOCKER-USER` по arch-06. + +## 12. Failure behavior + +- upstream down: bounded 502/504, без SPA fallback и без переноса на другую VM; +- cert renewal failed: текущий cert продолжает работу, alert до expiry; +- invalid new config: reload отменяется, старые workers остаются; +- disk/cache full: public cache bypass/evict, requests продолжаются где безопасно; +- DNS upstream changed: resolver/restart policy восстанавливает адрес; +- overload: 429/503 на edge, bounded queues; не накапливать неограниченные connections. + +## 13. Общий Definition of Done + +- на каждой VM ровно один nginx; независимые public `80/443` и собственные сертификаты; +- TLS/ACME bootstrap, renewal и safe reload испытаны; +- internal endpoints/ports извне недоступны; +- request id и trusted forwarded headers корректны; +- JSON logs коррелируют request/trace и не содержат секретов; +- health/synthetic checks и failure tests проходят; +- image non-root/read-only насколько возможно, versions pinned; +- runbooks для cert, reload, upstream outage и rollback готовы. + +Профильный DoD VM дополняет routing matrix, allow-list и listener этой машины. + +## 14. Решения, допущения и TBD + +**Решения:** независимые public nginx ВМ1/ВМ2; njs/module для UUID; public internal paths → 404; webroot ACME; CRM webhook приходит прямо на ВМ2; private `8443` ВМ2 только server-to-server. + +**Допущения:** ВМ1 и ВМ2 используют разные public hosts и сертификаты; upstream service names стабильны внутри каждого Compose; S3 CORS настраивается отдельно. + +**TBD:** + +- N1: доверенные WAF CIDR — по VM. +- N2: production cipher suite/OCSP. +- N3: нужен ли публичный health. +- N6: финальные burst/connection limits — по зонам VM. +- N7: certbot vs другой ACME client после ops review. + +N4 (CSP Expo) и N5 (Bitrix frame ancestors) — спецификация ВМ1. + +## 15. Ссылки + +- Compose/сети: [`arch-03-docker-compose-blueprint.md`](arch-03-docker-compose-blueprint.md). +- ВМ1: [`module-03-nginx-vm1.md`](../VM1_app/documentation/module-03-nginx-vm1.md). +- ВМ2: [`module-03-nginx-vm2.md`](../VM2_services/documentation/module-03-nginx-vm2.md). diff --git a/architectory/arch-09-redis.md b/architectory/arch-09-redis.md new file mode 100644 index 0000000..ca5a012 --- /dev/null +++ b/architectory/arch-09-redis.md @@ -0,0 +1,205 @@ +# arch-09. Контракт Redis + +> Канонический контракт Redis для всех application VM. +> Реализация на конкретной VM — [`module-04-redis-vm1.md`](../VM1_app/documentation/module-04-redis-vm1.md) и [`module-04-redis-vm2.md`](../VM2_services/documentation/module-04-redis-vm2.md). +> Compose/сети — [`arch-03-docker-compose-blueprint.md`](arch-03-docker-compose-blueprint.md). Env — [`arch-04-settings-and-content.md`](arch-04-settings-and-content.md). Метрики exporter — [`arch-07-observability.md`](arch-07-observability.md). + +## Назначение + +Документ фиксирует то, что **должно совпасть между ВМ1 и ВМ2**: Redis не source of truth, формат ключей, TTL, Lua governance, AOF/RDB, ACL/network, eviction и restore. Карты ключей DB0/DB1 и Redis Safety здесь не детализируются и не копируются в чужой репозиторий. + +Этот контракт **нельзя** независимо кастомизировать так, чтобы Redis стал очередью, OTP store или единственной защитой от дублей. + +## 1. Инварианты + +Redis разделён по deployment/security boundary: + +- Redis ВМ1: DB0 (`api-backend` idempotency/rate) и DB1 (realtime/coordination); +- Redis Safety ВМ2: отдельный instance для hot cache, rate limiting и optional worker wake-up; +- legacy DB2 ВМ1 существует только для test stub v1 до cutover и после него удаляется. + +Redis не является бизнес-очередью, source of truth сообщений, sync tasks, audit, профилей или delivery checkpoint. Надёжные состояния остаются в managed PostgreSQL/S3. Потеря Redis может ухудшить сервис, но не должна создавать потерю подтверждённых сообщений либо дубль side effect. + +OTP counters `api-backend` в Redis не хранит; они принадлежат Keycloak/SPI. `bitrix-sync` Redis не использует: `sync_queue`, leases и durable wake-up — PostgreSQL. + +Logical DB — изоляция имён, не security boundary. ACL prefix и разные credentials обязательны. + +## 2. Версия и topology + +Redis 7.x, image закреплён по digest. На каждой VM одна нужная primary instance без replica/Sentinel в MVP. Клиенты используют connection pool, bounded timeouts и не выполняют опасные команды. + +ВМ2 никогда не использует hostname Redis ВМ1 и наоборот. + +## 3. Общие правила ключей + +Формат: `han:{domain}:{purpose}:{hashed-or-public-id}:{version}`. Только ASCII lowercase separators. Public UUID допустим; IP, phone, email, token, text и filename — только HMAC/SHA-256 с server-side pepper там, где нужна защита dictionary attack. + +- key length желательно ≤ 200 bytes; +- значения versioned (`v=1`); +- timestamps — Unix ms/seconds или RFC3339, формат фиксирован для каждого key; +- wildcard `KEYS` production запрещён; только `SCAN` для ops; +- каждый non-channel key имеет TTL, кроме явно обоснованных bounded structures; +- large payload/presigned URL/token/message text запрещены. + +Новый key без TTL запрещён contract test, кроме Pub/Sub channel (не key) и ops metadata с явным обоснованием. + +## 4. Serialization и limits + +- простые counters — integer; +- locks — opaque random 128-bit token; +- metadata — Redis HASH либо компактный JSON с `schema_version`; +- max value target 32 KiB, hard application guard 128 KiB; +- response cache хранит только allow-listed sanitized JSON; +- decode error считается cache miss, key удаляется/карантинируется и поднимается metric. + +## 5. TTL policy — реестр + +| Категория | TTL | Владелец | +|---|---|---| +| idempotency completed | 24h по arch-02 | ВМ1 | +| idempotency in-progress lock | 30s, heartbeat bounded | ВМ1 | +| rate limit | window + 10–30% deterministic jitter | ВМ1 / ВМ2 по своим зонам | +| realtime connection | 90s; set membership 120s | ВМ1 | +| coordination lock | 30s | ВМ1 | +| safety file hot cache | ≤30d; authoritative row/version в PostgreSQL | ВМ2 | +| safety text-rules cache | 48h; invalidation by rules version | ВМ2 | +| safety stable link policy cache | 48h | ВМ2 | +| safety DNS cache | actual DNS TTL, hard max 900s | ВМ2 | + +## 6. Atomicity и Lua governance + +Scripts/functions хранятся в репозитории рядом с клиентом, versioned и тестируются на real Redis. Запрещены unbounded loops/SCAN внутри Lua. Входные массивы ограничены. Script timeout отслеживается; `SCRIPT KILL` runbook применяется только если нет writes либо после оценки. + +Clock в rate/limit scripts — Redis `TIME`, не client wall clock. Script загружается при startup, SHA кэшируется; после `NOSCRIPT` выполняется контролируемый reload. + +Redis transaction не координирует PostgreSQL/S3/HTTP. Cross-system consistency обеспечивается DB checkpoint/outbox и idempotent finalize. + +Карта обязательных scripts — в спецификации VM. + +## 7. Persistence + +Решение MVP: AOF `appendonly yes`, `appendfsync everysec` плюс RDB snapshots (`save 900 1`, `300 100`, `60 10000` либо tuned). Это ускоряет восстановление ephemeral state, но не превращает Redis в authoritative store. + +`aof-use-rdb-preamble yes`, automatic rewrite с порогами; volume `redis-data`. При corruption используется `redis-check-aof`/restore clean instance, а сервисы восстанавливают authoritative state из PostgreSQL. + +RPO Redis до ~1 секунды приемлем, потому что бизнес-RPO задаётся PostgreSQL/S3. Backup Redis не обязателен для бизнес-восстановления, но периодическая копия RDB/AOF полезна для ops forensic без secrets. + +## 8. Memory и eviction + +`maxmemory` задаётся относительно container limit (ориентир 70–75%, оставляя overhead/fork). Начальная оценка для одной VM — 512 MiB, уточняется load test. + +Eviction MVP: `volatile-lru`/`volatile-ttl`, так как все application keys имеют TTL. `allkeys-lru` опасен для idempotency при memory pressure; `noeviction` может полностью закрыть writes. Окончательный выбор после нагрузки: предпочтительно `volatile-lru` + alerts, а при разделении instances DB0 idempotency получает отдельную noeviction policy. + +Контролируются `used_memory`, RSS, fragmentation, evicted_keys, expired_keys, key count/avg TTL по DB. OOM/eviction не должен создавать дубль бизнес-эффекта: это гарантирует PostgreSQL fallback, не Redis. + +## 9. Sizing + +Общая формула: + +```text +instance working set × 1.5 allocator/fragmentation × 1.3 growth reserve +``` + +Состав working set считает спецификация VM. Pub/Sub output buffers и slow consumers имеют hard/soft limits. Load test фиксирует peak RPS, connections, record size и AOF rewrite headroom. + +## 10. Auth, ACL и network boundary + +Оба Redis не публикуют `6379` на host и подключены только к local Docker `backend` своей VM. `protected-mode yes`, default user отключён. + +ACL users (имена общие, credentials разные на каждой VM): + +- application user — только свои key prefixes и command categories; +- `ops_health`: `PING`, ограниченный `INFO`; +- `redis_exporter` — только `INFO`/`PING` и безопасные latency/keyspace metrics (arch-07 §8.3). + +Redis ACL не ограничивает logical DB напрямую надёжно; key-prefix patterns и разные credentials обязательны. `SELECT` запрещается, клиент URL сразу задаёт DB, но ACL prefix остаётся основной защитой. + +Dangerous/admin commands (`FLUSHALL`, `FLUSHDB`, `CONFIG`, `MODULE`, broad `KEYS`, replication changes) запрещены application users; rename-command не считается основной защитой. + +Пароли сильные, только env/secret mount, rotation current/new через rolling deploy. Внутри одной VM TLS Redis опционален при закрытой Docker network; при выносе за host/VPC TLS обязателен (`rediss://`) и plaintext отключается. + +## 11. Docker/runtime + +```text +redis/ + docker-compose.yml + redis.conf + users.acl.template + scripts/ + tests/ +``` + +Compose: pinned Redis image, `expose: 6379`, без `ports`, `backend` network, `redis-data:/data`, config/ACL read-only, non-root UID, no-new-privileges, dropped capabilities, resource/memory/ulimit settings. + +Startup валидирует config и ACL, permissions volume, затем Redis. Healthcheck использует ACL health user и `redis-cli --no-auth-warning PING`, secret не печатается. Graceful stop timeout позволяет AOF flush. + +Credential URL не входит в общий публичный `.env` как секрет: secret file / arch-06. + +## 12. Health — общие правила + +`PING` проверяет liveness Redis; readiness приложений проверяет auth, correct DB и выполнение малого read/write/expire script без оставления key. + +При latency выше threshold clients используют short timeout/circuit, не создают бесконечные retry storms. Reconnect — exponential backoff+jitter. + +Degraded policy конкретного сервиса — спецификация VM. + +## 13. Backup и restore + +Redis backup не используется для бизнес restore. Runbook: + +1. остановить/изолировать corrupted instance; +2. при целостном AOF/RDB восстановить на отдельном instance и проверить; +3. иначе поднять пустой Redis; +4. приложения прогревают ephemeral state из PostgreSQL / reconnect; +5. не копировать Redis dump в небезопасное место: keys содержат UUID и hashed identifiers. + +Детали прогрева — спецификация VM. + +## 14. Metrics и alerts — общие + +- availability, commands/sec, latency percentiles; +- connected/blocked clients, rejected connections; +- memory/RSS/fragmentation, maxmemory ratio; +- evictions/expirations/keyspace hits/misses; +- AOF fsync latency/rewrite status/last save; +- replication metrics зарезервированы; +- key count/avg TTL по DB без key values; +- script errors/NOSCRIPT/slowlog; +- Pub/Sub subscribers/output buffer/slow disconnect. + +Alerts: unavailable, p99 latency, >80/90% memory, any sustained evictions, AOF error, no recent persistence, client buffer pressure, unexpected keys without TTL. + +Бизнес-метрики rate/idempotency/Safety cache — спецификация VM и arch-07. Keys/values не экспортируются. + +## 15. Общий Definition of Done + +- Lua scripts atomic, bounded, versioned и покрыты real Redis tests; +- AOF/RDB, volume, restart и clean-instance recovery проверены; +- maxmemory/eviction/resource limits основаны на load test либо явно TBD; +- ACL users и network isolation работают, порт не published; +- все application keys имеют TTL; +- logs/metrics не содержат secret/value/PII; +- Redis не используется как `sync_queue`, delivery queue, message/audit source of truth или OTP store. + +Профильный DoD VM дополняет свои prefixes, URL и degraded policy. + +## 16. Решения, допущения и TBD + +**Решения:** по одной primary instance на каждую VM без replica/Sentinel в MVP; AOF everysec + RDB; Pub/Sub best effort; PostgreSQL durable fallback; prefix ACL; все application keys с TTL. Историческая схема «один instance / три DB» заменена разделением ВМ1 DB0/DB1 и Redis Safety ВМ2. + +**Допущения:** Redis loss допустим без потери business truth; `bitrix-sync` Redis не использует. + +**TBD:** + +- R1: точный maxmemory после load profile — по VM. +- R2: eviction policy после измерений. +- R3: credential env names в arch-04. +- R5: TLS при изменении network topology. +- R6: момент разделения DB на instances — спецификация ВМ1. +- R7: RPO/RTO ops target. +- R4: Safety task TTL/recovery margin — спецификация ВМ2 (PostgreSQL, не Redis). + +## 17. Ссылки + +- ВМ1: [`module-04-redis-vm1.md`](../VM1_app/documentation/module-04-redis-vm1.md). +- ВМ2: [`module-04-redis-vm2.md`](../VM2_services/documentation/module-04-redis-vm2.md). diff --git a/architectory/arch-10-deployment.md b/architectory/arch-10-deployment.md new file mode 100644 index 0000000..1672cd0 --- /dev/null +++ b/architectory/arch-10-deployment.md @@ -0,0 +1,340 @@ +# arch-10. Контракт развёртывания + +> Канонический контракт rollout для всех application VM. OS-роли, SSH/sudo, secrets delivery и hardening — [`arch-06-service-hosting-security.md`](arch-06-service-hosting-security.md); этот документ их не ослабляет. +> Процедуры конкретной машины — [`module-10-deployment-vm1.md`](../VM1_app/documentation/module-10-deployment-vm1.md) и [`module-10-deployment-vm2.md`](../VM2_services/documentation/module-10-deployment-vm2.md). +> Nginx TLS — [`arch-08-nginx.md`](arch-08-nginx.md). Redis — [`arch-09-redis.md`](arch-09-redis.md). Telemetry — [`arch-07-observability.md`](arch-07-observability.md). Compose — [`arch-03-docker-compose-blueprint.md`](arch-03-docker-compose-blueprint.md). + +## Назначение + +Документ фиксирует то, что **должно совпасть между ВМ1 и ВМ2** и то, что связывает их как систему: независимый deploy, VPC/SG, managed PG, S3, роли `deploy`/`admin`, секреты, TLS/ACME процедура, порядок cutover Safety, backup/rollback/DR. + +Команды сервисов конкретной машины и её Compose **не** дублируются здесь. Агент репозитория VM читает этот контракт плюс свой runbook. + +Прямые `docker compose` команды выполняются `admin` только при bootstrap/recovery либо инкапсулируются в утверждённые root-owned systemd-units. Они не являются основанием выдавать `deploy` доступ к Docker daemon. Значения в `<УГЛОВЫХ_СКОБКАХ>` — placeholders. + +## 1. Неподвижные правила + +1. Один root Compose project описывается в `` своей VM; в steady state его запускает root-owned systemd-unit/helper, а не пользователь из группы `docker`. +2. На каждой VM ровно один nginx; ВМ1 и ВМ2 имеют независимые public DNS/TLS ingress и deployment lifecycle. +3. Application containers не имеют public host ports. Nginx ВМ1 публикует свой `80/443`; nginx ВМ2 — отдельный `80/443` только для exact CRM webhook и private `8443` для Message Safety/internal access. +4. `/internal/*` не маршрутизируется публично. +5. Managed PostgreSQL находится вне Compose, в той же VPC, без public IP. +6. S3 — внешний Selectel-compatible storage; клиент получает только presigned URL. +7. Секреты не коммитятся, не вставляются в команды shell history и не выводятся в отчёты. +8. Миграции выполняются отдельными one-shot steps до новой версии приложения. +9. Message Safety запускается как documented stub до замены; это не production antivirus/moderation. +10. `bitrix-sync` вводится только после выполнения preflight/cutover gates module-07; до этого `BITRIX_SYNC_ENABLED=false`, public webhook закрыт на edge. +11. На ВМ1 и ВМ2 отдельные root Compose projects/systemd units; deploy/rollback выполняются независимо. +12. ВМ2 — самостоятельная service VM с минимальным public webhook ingress, allow-listed egress, отдельным IAM principal и service-specific secret files. +13. OS-роли, SSH/sudo, secrets delivery, container hardening и private-VM lockdown подчиняются arch-06. + +## 2. Роли и обозначения + +- **Cloud admin**: VPC, VM, PG, S3, DNS/security groups. +- **Deploy operator / OS user `deploy`**: запуск утверждённых release/rollback/migration systemd-units; без группы `docker`, записи в production compose/unit/scripts/config и общего sudo. +- **Break-glass OS user `admin`**: bootstrap и аварийное восстановление; не используется для штатного деплоя. +- **OS user `tunnel`**: только allow-listed local TCP forwarding к private endpoints; без sudo/shell operations. +- **Bitrix admin**: local app, connector, Open Line 8, callbacks. +- **Security owner**: secrets, Keycloak admin MFA, firewall, retention. +- **Safety Service Owner**: API/data contract, capacity result и v2 cutover/rollback sign-off. +- **Rule Pack Owner**: rules bundle, corpus, monitor report и version release. +- **Product Owner**: mnemonic `safety.chat.blocked` и business acceptance chat flow. +- **Operations Owner**: VM2 alerts, ClamAV signatures, incident/reprovision/restore rehearsal. + +```text + например chat.example.ru + публичный IPv4 ВМ1 + отдельный public host ВМ2, например processing.example.ru + публичный IPv4/LB address ВМ2 + приватный IPv4 VM + например 10.20.0.0/24 + private FQDN/IP managed PG + 5432 или 6432 + han_chat + URL репозитория этой VM + /opt/han-chat/backend + immutable tag/git SHA + адрес ops, не placeholder в реальном запуске + разрешённый портал + согласованное в Direct имя отправителя + фактический статический egress IP `sms-worker` + контролируемый номер для provider smoke +``` + +`` и `` **разные** у ВМ1 и ВМ2. + +## 3. Stage 0 — решения до provisioning + +Зафиксировать: region/AZ и VPC; hostnames и TTL DNS; VM image Ubuntu 24.04 LTS; sizing каждой VM; PG plan/storage/backups/PITR; S3 region/endpoint/bucket names; container registry и immutable image tags/digests; remote observability backend; RPO/RTO и maintenance window; ответственных за alerts/Bitrix/Keycloak; Safety Service/Rule Pack/Product/Security/Operations owners и approvals cutover. + +Sizing и load gates конкретной машины — профильный runbook. Monthly availability SLO для MVP не задаётся; это не отменяет latency/load gates и alerts. + +### Gate 0 + +- [ ] Владельцы и maintenance window назначены. +- [ ] RPO/RTO приняты хотя бы временно: ориентир RPO PG ≤15 минут/PITR, RTO ≤4 часа. +- [ ] Решено: images pull из registry или build на VM. +- [ ] Remote telemetry backend выбран либо явно принят ограниченный debug-only режим. +- [ ] Риск mock OTP до SMS cutover и Safety stub письменно принят; real SMS не включается без gates module-11. + +**Ожидаемый результат:** есть release checklist с конкретными values; не создано ни одной публичной БД/Redis. + +## 4. Stage 1 — VPC, DNS и security groups + +Создать private subnet для ВМ1, ВМ2, SigNoz и managed PG. ВМ1 и ВМ2 имеют отдельные public IP/LB только для своих nginx; service-to-service и PostgreSQL traffic остаётся private. SSH к обеим VM — только ops VPN/bastion. + +| Source | Destination | Port | Rule | +|---|---|---:|---| +| trusted ops CIDR/VPN | ВМ1, ВМ2 | SSH `` | allow | +| internet | ВМ1 | TCP 80/443 | allow edge redirect/ACME/application | +| internet | nginx ВМ2 | TCP 80/443 | allow ACME/redirect + exact CRM webhook | +| ВМ1 SG | ВМ2 | TCP 8443 | private TLS only | +| ВМ1/ВМ2 service SG | managed PG | `` | allow по нужным DB roles | +| ВМ2 collector | private SigNoz | TCP 4317 | allow | +| ВМ2 workers | S3 endpoints | TCP 443 | allow | +| ВМ2 `bitrix-sync` | approved Bitrix portal | TCP 443 | allow | +| ВМ2 `freshclam` | approved signature CDN | TCP 443/80 по vendor manifest | allow | +| ВМ2 | trusted DNS/NTP | UDP/TCP 53, UDP 123 | allow | +| internet | managed PG | any | deny | +| internet | ВМ2 | any кроме nginx 80/443 | deny ingress | +| internet | обе VM | 6379, 4317, 4318, 8000, 8080, 8443, 9000 | deny public | + +ВМ2 использует default-deny egress. Registry/OS repositories открываются только в bootstrap/controlled window и затем снова закрываются. Если provider SG не умеет destination allow-list, применяется host firewall/proxy/NAT policy; постоянный open egress для ВМ2 не является допустимым production состоянием. + +DNS: `A `, `A `, private DNS `processing.internal → `. Public host ВМ2 используется только CRM webhook; private name не публикуется во внешнем DNS. + +### Gate 1 + +- [ ] PG не имеет public endpoint. +- [ ] SSH доступен только trusted source. +- [ ] Снаружи открыты только 80/443/ограниченный SSH. +- [ ] DNS стабильно разрешается с нескольких resolver. +- [ ] Каждая VM достигает private PG и своих внешних HTTPS endpoints. + +**Ожидаемый результат:** `nc -vz ` с VM успешен; с внешней машины PG недоступен. + +## 5. Stage 2 — hardening Ubuntu + +Процедура первичная для **каждой** application VM. Скрипт-прототип [`../../HAN_chat/deploy/setup-vm-han-chat.sh`](../../HAN_chat/deploy/setup-vm-han-chat.sh) полезен для UFW, fail2ban, Docker и `DOCKER-USER`. Перед production: review версии; не передавать IP/ключи в git; проверить unattended upgrades; `deploy` не в группе `docker`; `AllowTcpForwarding no` по умолчанию; отдельный `tunnel` при необходимости; root-owned systemd-units и `/etc/sudoers.d/deploy` без wildcard. + +`PUBLIC_DOCKER_PORTS` задаёт профильный runbook (ВМ1: `80,443`; ВМ2 — свои public 80/443, private 8443 не internet). + +Проверки: `sshd -t`, UFW, fail2ban, Docker/Compose versions, `iptables -L HAN-CHAT-DOCKER`, timedatectl, disk/swap. Второй SSH session как `deploy`, затем отключить root/password login. + +### Gate 2 + +- [ ] SSH key login `deploy` проверен во втором сеансе. +- [ ] Root/password auth выключены. +- [ ] `deploy` не состоит в группе `docker`; `sudo -l` содержит только утверждённые конкретные systemd-команды. +- [ ] Production compose, units, deploy scripts и secret mappings принадлежат root и недоступны `deploy` на запись. +- [ ] UFW и DOCKER-USER активны после restart Docker. +- [ ] Для published Docker ports allow rules сопоставляют original host destination через `conntrack --ctorigdstport`; positive/negative probes увеличивают counters нужных allow/deny rules после restart Docker и reboot. +- [ ] После обновления firewall helper active `oneshot RemainAfterExit` unit явно перезапущен; `enable --now` не считается применением новой версии. +- [ ] Docker Engine/Compose plugin закреплены поддерживаемой версией. +- [ ] NTP active; disk/swap соответствуют sizing. +- [ ] Break-glass процедура сохранена вне VM. + +**Ожидаемый результат:** reboot VM не теряет SSH, firewall и Docker service. + +### 5.1. Lockdown private/no-egress VM + +Для SigNoz и другой VM, которая после раскатки не должна иметь internet ingress/egress, bootstrap выполняется по lifecycle arch-06. Checklist закрытия public IP/SSH/egress и запрет постоянного open egress — arch-06; повторное открытие только break-glass с повтором lockdown. + +## 6. Stage 3 — managed PostgreSQL + +До создания схем включить daily backup, PITR, encryption at rest, TLS `sslmode=verify-full` + provider CA, alerts (disk/connections/CPU/IO/replication/backup/CA), deletion protection. + +CA: `/opt/han-chat/secrets/pg/ca.pem`, `root:deploy` `0440` (либо `0400`). Режимы `disable`/`allow`/`prefer`/`require` без проверки CA для production запрещены. + +Публичные HTTPS-сертификаты `` и `` выпускаются отдельно через Let's Encrypt на Stage 9 nginx соответствующей VM, не в PostgreSQL. + +Целевая модель ролей: admin/bootstrap; migration role каждого schema с DDL; runtime без DDL. Прототип `init-managed-postgres.py` слишком широк для production runtime. + +Шесть schemas: `han_app`, `bitrix_local`, `bitrix_sync`, `keycloak`, `message_safety`, `sms`. Schema owner = migration role; runtime: `USAGE`, DML и sequence grants только на свои objects; `bitrix_sync_user` — только grants module-07 §13. + +Ownership миграций: + +1. `api-backend` Alembic — `han_app`; +2. `bitrix-local-app` — `bitrix_local`; +3. `message-safety` stub не создаёт PG tables до production implementation; +4. `bitrix-sync` — schema `bitrix_sync`; api-backend отдельно мигрирует shared `han_app.sync_queue`; +5. Keycloak — standard tables; custom provider — свои migrations; +6. `sms-service` — schema `sms`; runtime `sms_user` без доступа к `han_app`/`keycloak`. + +Только expand/migrate/contract. Destructive migration — отдельный backup, approval и release. Downgrade data migrations не обещается. + +### Gate 3 + +- [ ] Backups/PITR/TLS/deletion protection включены. +- [ ] Шесть schemas/roles созданы, включая `sms`/`sms_user`. +- [ ] Runtime roles не имеют DDL/чужого доступа. +- [ ] Migration credentials отделены от runtime. +- [ ] Empty/previous-version migration test успешен. +- [ ] PITR restore point создан перед первым release. + +## 7. Stage 4 — S3 + +Три приватных bucket: `-quarantine`, `-attachments`, `-documents`. Public ACL/listing выключены. Versioning для data buckets по policy; SSE включить. + +IAM: API role — exact prefixes, presign PUT quarantine, Head/copy/delete quarantine, write/read data; Safety role — **read-only quarantine**; backup/ops отдельно; frontend — никаких permanent credentials. Отдельный IAM principal ВМ2. + +CORS quarantine: `AllowedOrigins` только `https://`; PUT; headers `Content-Type`, `If-None-Match`, checksum; не `*` origin с credentials. + +Lifecycle: quarantine failed/orphan expire 48 ч только при отсутствии active `safety_tasks`; incomplete multipart abort 1 день; attachments/documents без auto-delete до legal retention. + +### Gate 4 + +- [ ] Все buckets private. +- [ ] API key не может list/write вне exact scope. +- [ ] Safety key не может write/delete. +- [ ] Browser test origin выполняет presigned PUT с checksum и `If-None-Match: *`; повтор того же key получает `412`. +- [ ] Complete фиксирует authoritative `version_id`, ETag и checksum; Safety читает только эту version. +- [ ] Wrong version/ETag и изменённый source дают deny/error и не promote-ятся. +- [ ] Conditional promote mismatch не создаёт delivery outbox/Bitrix call. +- [ ] Quarantine lifecycle не удалит active `safety_tasks`. +- [ ] Data lifecycle соответствует retention. + +## 8. Общий layout релиза и секретов + +На каждой VM: + +```text +/opt/han-chat/ + backend/ # checkout текущего release + releases// # optional immutable release dirs + secrets/ # не в git + backups/ # только metadata/short-lived encrypted artifacts +``` + +Рекомендуемый rollout — immutable images из registry. Deploy из mutable branch без recorded SHA запрещён. `latest` отсутствует. + +`.env` — только несекретный config и `SECRETS_SOURCE=file|selectel`. Секреты выдаёт `deployment/secrets/han-secrets`. `docker compose config` может раскрыть secrets; stdout не публиковать. + +Validation: нет `change-me`, paired tokens equal, PG TLS, public HTTPS, `FRONTEND_DEV_PROXY_ENABLED=false` на production-like. + +Состав `.env`/secret groups — профильный runbook. + +## 9. TLS/ACME процедура + +Webroot two-phase, независимо в root Compose каждой VM, без `compose down`. Staging CA rehearsal, затем production `--cert-name` текущего host. Сертификат и ACME volume между VM не разделяются. + +Renew: systemd timer дважды в сутки. Скрипт `/deploy/ssl-renew.sh`: + +1. взять `flock`; +2. `docker compose --profile certbot run --rm certbot renew --webroot -w /var/www/certbot --quiet`; +3. при обновлении проверить `docker compose exec -T nginx nginx -t -c /tmp/nginx.conf`; +4. только после успеха `docker compose kill -s HUP nginx`; +5. записать результат и метрику expiry; +6. ненулевой exit при ошибке; +7. не удалять действующий сертификат; +8. success path — `0` и пустой stderr. + +Пример unit `/etc/systemd/system/han-chat-cert-renew.service`: + +```ini +[Unit] +Description=Renew HAN Chat Let's Encrypt certificate +Requires=docker.service +After=docker.service network-online.target + +[Service] +Type=oneshot +User=deploy +WorkingDirectory= +ExecStart=/deploy/ssl-renew.sh +``` + +Timer `OnCalendar=*-*-* 03,15:20:00`, `RandomizedDelaySec=30m`, `Persistent=true`. `enable --now`, `list-timers`, `certbot renew --dry-run`. Alert <21 дней, page <7 дней. Ошибка renew не останавливает nginx. + +Staging issuance, затем production `--cert-name` текущего host. Host для ВМ1 — ``; для ВМ2 — ``; private `8443` — internal CA, не Let's Encrypt. Сертификат и ACME volume между VM не разделяются. + +## 10. Сквозной порядок startup и cutover + +1. На ВМ2 unit поднимает Redis Safety и local Collector. +2. Затем `clamd`/`freshclam`, Safety API/worker и `bitrix-sync`. +3. Последним на ВМ2 — nginx public `80/443` и private `8443`. +4. На ВМ1 — Redis/Collector, API, SMS, Keycloak, local app, edge nginx. +5. Только private `MESSAGE_SAFETY_URL` ВМ1 переключается на ВМ2 после Safety gates. Public CRM webhook DNS/routes ВМ2 не требуют изменения ВМ1. + +Legacy single-VM `docker compose up` не является evidence готовности target ВМ2. Один `message_id` нельзя одновременно отправлять в v1 и v2. После cutover ВМ1 не содержит local Safety/Redis DB2. + +При потере ВМ2 fail-open запрещён. Reprovision ВМ2 из immutable image/config; Redis пустой; gates повторяются. RTO ≤4 ч; restore rehearsal минимум дважды в год. + +## 11. Observability, opening traffic, backup, rollback + +Gate 5–14 не дублируются в этом общем контракте: это профильные release gates конкретной VM, описанные в `module-10-deployment-vm1.md` / `module-10-deployment-vm2.md` и executable runbook соответствующего репозитория. Нумерация внутри VM2 runbook локальна его технической приёмке; системное открытие трафика всегда требует подписанных результатов обоих контуров. + +**Gate 15 — observability:** arch-07 и профильный module-09 VM spec; сквозной `request_id` через nginx ВМ1 → API → Safety ВМ2 обязателен до объявления production-ready. + +**Gate 16 — opening traffic:** снять maintenance, включить HSTS после TLS test, сохранить digests/revisions, restore point и on-call, затем наблюдать 60 минут. До открытия должны быть подписаны Gate 0–4 этого документа, применимые Gate 5–14 профильных runbook обеих VM и Gate 15. + +Backup: PG daily+PITR — business restore; S3 versioning; Redis не business backup; Keycloak в PG. Restore rehearsal в isolated VPC без production DNS/Bitrix callbacks. + +Rollback приложения: previous immutable digests, без Alembic downgrade, `SCHEMA_BACKWARD_COMPATIBLE_CONFIRMED=true`. Backward-incompatible migration — только forward-fix или PITR. TLS: оставить старый config/cert при failure. + +Запрещены: `docker compose down -v`, `docker system prune -a --volumes`, `DROP DATABASE` / `DROP SCHEMA`, recursive S3 delete, `certbot delete` active cert, unbounded logs, Redis `KEYS/FLUSH*`, ad-hoc DB DELETE. + +Routine (автомат ежедневно): health/synthetics, PG backup/PITR, TLS expiry, disk/OTEL queue, Redis AOF/memory, DLQ/quarantine age. Еженедельно: image CVE, login/rate-limit trend, S3 inventory. Ежемесячно: OS patch, secret review, capacity, runbook drill. + +```bash +cd +docker compose ps +docker compose logs --since=15m +docker stats --no-stream +docker system df +docker compose exec -T nginx nginx -t +``` + +Incident triage: + +```bash +cd +date -Is +docker compose ps +docker stats --no-stream +docker compose logs --since=10m --tail=500 +df -h +free -h +sudo ss -lntp +sudo iptables -L HAN-CHAT-DOCKER -n -v +``` + +PG: readonly `select now(), count(*) from pg_stat_activity`. Redis — только ops ACL `PING`. Secret literal не вставлять в ticket. Не replay message/DLQ до idempotency и ambiguous Bitrix outcome. + +Типовые сценарии: API 503 — DB/Redis/JWKS/Safety circuits; send timeout — checkpoint, не новый key; Bitrix down — OAuth/circuit/DLQ; Redis loss — clean restart + polling; PG outage — не restart storm; disk full — known cache/old images после inventory; cert expiry — webroot/staging; secret leak — rotate, telemetry deletion. + +Upgrades: notes → compatibility → PITR → staging → expand migration → one service at a time → E2E → contract later. Keycloak не пропускать unsupported majors. + +DR потеря VM: новая Ubuntu в VPC, hardening, DNS, secrets из vault не со старого disk, exact images, Redis можно clean, existing PG/S3, TLS, ordered startup. Потеря PG: PITR в new instance, остановить writes, reconcile S3 orphans. Потеря S3: без versioning полное восстановление невозможно; отключить file ops. Compromise: isolate, forensic snapshot, rotate all, clean deploy, notify. + +Перед teardown: inventory без secrets, revoke Bitrix/credentials, legal hold, DNS drain, deletion protection — отдельное approval, shared VPC/PG/S3 проверить. + +## 12. Общий Definition of Done + +- VM/VPC/DNS/SG/hardening соответствуют Gate 1–2; +- managed PG private/TLS/backups/least privilege/migrations работают; +- S3 private/IAM/CORS/lifecycle проверены; +- `deploy` не имеет Docker/root-equivalent доступа; +- независимые public 80/443; ВМ2 дополнительно private 8443; +- после Safety cutover ВМ1 вызывает ВМ2 только по private HTTPS с проверенным CA bind; +- container hardening — arch-06; +- для каждой private/no-egress VM завершён lockdown; +- backup restore и rollback rehearsed; +- ops/incident/upgrade/DR owners назначены; +- все assumptions/TBD приняты до открытия traffic. + +Профильный DoD VM дополняет свои сервисы, Compose и cutover steps. + +## 13. Допущения, TBD и конфликты + +**Допущения:** D-A1 отдельные public hosts; D-A2 обязательный минимум — local Collector; D-A3 managed PG private/TLS/PITR; D-A4 Bitrix portal/connector/line — значения architecture; D-A5 mock OTP до SMS cutover как documented risk. + +**TBD:** D-TBD1 domains/Expo redirect/Bitrix frame ancestors; D-TBD2 final VM1/PG sizing, public SLO, RPO/RTO; D-TBD3 legal retention; D-TBD4 secret manager; D-TBD5 Safety v2 cutover; D-TBD6 bitrix-sync cutover; D-TBD7 pinned versions; D-TBD8 exact CLI после реализации; D-TBD9 Keycloak admin VPN/MFA; D-TBD10 CSP/CORS/IAM; D-TBD11 SigNoz auth/retention/alert route. + +**Конфликты:** module-07 documentation ≠ cutover; v1 stub ≠ production Safety; prototype PG init слишком широк; prototype TLS downtime; prototype `/bitrix-internal/*` запрещён; Compose/env могут ещё не содержать VM2/SMS artifacts. + +## 14. Ссылки + +- ВМ1: [`module-10-deployment-vm1.md`](../VM1_app/documentation/module-10-deployment-vm1.md). +- ВМ2: [`module-10-deployment-vm2.md`](../VM2_services/documentation/module-10-deployment-vm2.md). +- Прототипы: [`../../HAN_chat/Deploy_steps.md`](../../HAN_chat/Deploy_steps.md), [`../../HAN_chat/deploy/setup-vm-han-chat.sh`](../../HAN_chat/deploy/setup-vm-han-chat.sh), [`../../HAN_chat/deploy/init-managed-postgres.py`](../../HAN_chat/deploy/init-managed-postgres.py). diff --git a/infratest/.env.example b/archive/infratest/.env.example similarity index 100% rename from infratest/.env.example rename to archive/infratest/.env.example diff --git a/infratest/.gitignore b/archive/infratest/.gitignore similarity index 100% rename from infratest/.gitignore rename to archive/infratest/.gitignore diff --git a/infratest/README.md b/archive/infratest/README.md similarity index 100% rename from infratest/README.md rename to archive/infratest/README.md diff --git a/infratest/infratest.py b/archive/infratest/infratest.py similarity index 100% rename from infratest/infratest.py rename to archive/infratest/infratest.py diff --git a/infratest/requirements.txt b/archive/infratest/requirements.txt similarity index 100% rename from infratest/requirements.txt rename to archive/infratest/requirements.txt diff --git a/infratest/test_infratest.py b/archive/infratest/test_infratest.py similarity index 100% rename from infratest/test_infratest.py rename to archive/infratest/test_infratest.py diff --git a/archive/module-03-nginx.md b/archive/module-03-nginx.md new file mode 100644 index 0000000..99bd714 --- /dev/null +++ b/archive/module-03-nginx.md @@ -0,0 +1,14 @@ +# module-03. Проектная спецификация корневого `nginx` + +> Статус: указатель. Канонический контракт и профильные спецификации VM разнесены. +> Этот файл сохраняет стабильный путь `module-03-nginx.md` для существующих ссылок. + +Nginx разрезан так, чтобы репозитории ВМ1 и ВМ2 не тащили чужой routing, а TLS, request id, ACME и запрет `/internal/` не разъезжались. + +| Документ | Роль | +|---|---| +| [`../architectory/arch-08-nginx.md`](../architectory/arch-08-nginx.md) | Архитектурный контракт: независимый nginx на каждой VM, TLS/ACME, request id, forwarded headers, internal 404, JSON access log, Compose hardening. Не кастомизировать в репозитории VM так, чтобы сломать контракт. Compose-сети и published ports остаются в [`../architectory/arch-03-docker-compose-blueprint.md`](../architectory/arch-03-docker-compose-blueprint.md). | +| [`module-03-nginx-vm1.md`](module-03-nginx-vm1.md) | Реализация на ВМ1: SPA, `/api/`, `/auth/`, WS, Bitrix local app, SMS callback, CSP/CORS, public cache. | +| [`module-03-nginx-vm2.md`](module-03-nginx-vm2.md) | Реализация на ВМ2: exact CRM webhook, source IP allow-list, private `8443` Message Safety. | + +Агент ВМ1 читает arch-08 + спецификацию ВМ1. Агент ВМ2 читает arch-08 + спецификацию ВМ2. Публичный трафик одной машины не проксируется через другую. diff --git a/archive/module-04-redis.md b/archive/module-04-redis.md new file mode 100644 index 0000000..26b8eef --- /dev/null +++ b/archive/module-04-redis.md @@ -0,0 +1,14 @@ +# module-04. Проектная спецификация Redis + +> Статус: указатель. Канонический контракт и профильные спецификации VM разнесены. +> Этот файл сохраняет стабильный путь `module-04-redis.md` для существующих ссылок. + +Redis разрезан так, чтобы репозитории ВМ1 и ВМ2 не тащили чужие ключи и URL, а инварианты (не source of truth, TTL, ACL prefix, AOF) не разъезжались. + +| Документ | Роль | +|---|---| +| [`../architectory/arch-09-redis.md`](../architectory/arch-09-redis.md) | Архитектурный контракт: Redis не очередь и не OTP store, формат ключей, Lua governance, AOF/RDB, ACL/network, eviction, restore. Не кастомизировать в репозитории VM так, чтобы сломать контракт. | +| [`module-04-redis-vm1.md`](module-04-redis-vm1.md) | Реализация на ВМ1: DB0 rate/idempotency, DB1 realtime/coordination, legacy DB2 stub до cutover. | +| [`module-04-redis-vm2.md`](module-04-redis-vm2.md) | Реализация на ВМ2: Redis Safety hot cache/rate/wakeup; task/lease остаются в PostgreSQL. | + +Агент ВМ1 читает arch-09 + спецификацию ВМ1. Агент ВМ2 читает arch-09 + спецификацию ВМ2. Экземпляры не шарят hostname, volume и credentials. diff --git a/archive/module-09-observability.md b/archive/module-09-observability.md new file mode 100644 index 0000000..691923d --- /dev/null +++ b/archive/module-09-observability.md @@ -0,0 +1,14 @@ +# module-09. Наблюдаемость production-like контура + +> Статус: указатель. Канонический контракт и профильные спецификации VM разнесены. +> Этот файл сохраняет стабильный путь `module-09-observability.md` для существующих ссылок. + +Наблюдаемость разрезана так, чтобы репозитории ВМ1 и ВМ2 не тащили чужой контур, а общие поля логов и redaction не разъезжались. + +| Документ | Роль | +|---|---| +| [`../architectory/arch-07-observability.md`](../architectory/arch-07-observability.md) | Архитектурный контракт: Collector, JSON stdout, resource attributes, W3C, redaction, sampling, SLO, SigNoz, общие runbooks. Не кастомизировать в репозитории VM. | +| [`module-09-observability-vm1.md`](module-09-observability-vm1.md) | Реализация на ВМ1: nginx edge, `api-backend`, Keycloak, `bitrix-local-app`, Redis приложения, дашборды и алерты этой машины. | +| [`module-09-observability-vm2.md`](module-09-observability-vm2.md) | Реализация на ВМ2: nginx webhook/private, `message-safety`, `bitrix-sync`, Redis Safety, дашборды и алерты этой машины. | + +Агент ВМ1 читает arch-07 + спецификацию ВМ1. Агент ВМ2 читает arch-07 + спецификацию ВМ2. Сквозной путь «frontend → nginx → API → Safety → Open Lines» связывается `request_id` / `trace_id`, а не общим Compose. diff --git a/archive/module-10-deployment-runbook.md b/archive/module-10-deployment-runbook.md new file mode 100644 index 0000000..0b11ab0 --- /dev/null +++ b/archive/module-10-deployment-runbook.md @@ -0,0 +1,15 @@ +# module-10. Runbook развёртывания HAN Chat + +> Статус: указатель. Канонический контракт и профильные runbook VM разнесены. +> Этот файл сохраняет стабильный путь `module-10-deployment-runbook.md` для существующих ссылок. +> Команды существующего stub-контура применимы только до production Safety cutover и явно отмечены как legacy в профильных файлах. + +Развёртывание разрезано так, чтобы репозитории ВМ1 и ВМ2 не тащили чужой Compose и секреты, а VPC, managed PG, S3, роли `deploy` и порядок Safety cutover не разъезжались. + +| Документ | Роль | +|---|---| +| [`../architectory/arch-10-deployment.md`](../architectory/arch-10-deployment.md) | Контракт: независимый deploy, SG/DNS, PG/S3, hardening, TLS процедура, сквозной startup/cutover, backup/rollback/DR. Не кастомизировать в репозитории VM так, чтобы сломать границы. OS-роли — [`../architectory/arch-06-service-hosting-security.md`](../architectory/arch-06-service-hosting-security.md). | +| [`module-10-deployment-vm1.md`](module-10-deployment-vm1.md) | Runbook ВМ1: edge, API, Keycloak, SMS, local app, frontend, legacy Safety gate, `MESSAGE_SAFETY_URL`. | +| [`module-10-deployment-vm2.md`](module-10-deployment-vm2.md) | Runbook ВМ2: webhook/private nginx, Safety, ClamAV, `bitrix-sync`, MOCK, Freshclam, reprovision. | + +Агент ВМ1 читает arch-10 + runbook ВМ1. Агент ВМ2 читает arch-10 + runbook ВМ2. Сквозной Gate 14/15/cutover требует оба контура, но выполняется разными репозиториями и systemd-units. diff --git a/modules/sync-service-concept.md b/archive/sync-service-concept.md similarity index 100% rename from modules/sync-service-concept.md rename to archive/sync-service-concept.md diff --git a/codebase/README.md b/codebase/README.md deleted file mode 100644 index 586bff2..0000000 --- a/codebase/README.md +++ /dev/null @@ -1,13 +0,0 @@ -# HAN Chat MVP - -Production-like MVP implementation described by `../architectory` and `../modules`. - -The current executable entry point is `backend/docker-compose.yml`; it is the -legacy VM1/stub contour, not evidence that the VM2 cutover is complete. - -Target production has two independent root Compose projects/systemd units: -VM1 HAN Chat (`backend/`) and private VM2 Processing (`message-safety`, -`bitrix-sync`, ClamAV, Redis Safety, internal nginx and a local OTEL Collector). -Until the VM2 project is implemented, follow the existing backend guide only -for development/acceptance and the target runbook in -`../modules/module-10-deployment-runbook.md` for migration boundaries. diff --git a/modules/Untitled b/modules/Untitled deleted file mode 100644 index 08e8bdc..0000000 --- a/modules/Untitled +++ /dev/null @@ -1 +0,0 @@ -bitrix_local \ No newline at end of file diff --git a/modules/module-03-nginx.md b/modules/module-03-nginx.md deleted file mode 100644 index 638eb42..0000000 --- a/modules/module-03-nginx.md +++ /dev/null @@ -1,355 +0,0 @@ -# module-03. Проектная спецификация корневого `nginx` - -> Статус: целевая спецификация независимых nginx-контуров ВМ1 и ВМ2. -> Источники: [`README.md`](README.md), [`arch-00-glossary.md`](arch-00-glossary.md), [`arch-01-system-architecture.md`](arch-01-system-architecture.md), [`arch-02-api-contracts.md`](arch-02-api-contracts.md), [`arch-03-docker-compose-blueprint.md`](arch-03-docker-compose-blueprint.md), [`arch-04-settings-and-content.md`](arch-04-settings-and-content.md), [`arch-05-agent-development-process.md`](arch-05-agent-development-process.md), [`module-01-api-backend.md`](module-01-api-backend.md). - -## 1. Назначение и обязательная топология - -В production-like контуре каждая VM имеет собственный nginx в своём root Compose и собственный deployment lifecycle: - -- nginx ВМ1 обслуживает frontend/API/auth/Open Lines/SMS; -- nginx ВМ2 напрямую обслуживает публичные CRM webhook `bitrix-sync` и private Message Safety API; -- публичный трафик ВМ2 не проходит через ВМ1; -- отказ или deploy ВМ1 не прерывает приём CRM webhook на ВМ2. - -Контейнеры приложений не публикуют host ports. На каждой VM наружу смотрит только её nginx. Если перед конкретной VM есть WAF/LB, trusted proxy CIDR задаются отдельно. - -## 2. Routing matrix - -Порядок location критичен. Публичные route разделены по host/VM. - -### ВМ1 - -| Внешний путь | Upstream | Режим | -|---|---|---| -| `/api/` | `api-backend:8000` | REST; `/api/v1/realtime` WS | -| `/auth/` | `keycloak:8080` | OIDC/OTP, prefix/hostname согласован с issuer | -| 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 | -| `/` | static SPA либо Expo dev upstream | `try_files` fallback | - -### ВМ2 - -| Внешний путь | Upstream | Режим | -|---|---|---| -| exact `/bitrix/sync/webhook/contact` | `bitrix-sync:8080` | public HTTPS Contact event; source IP CIDR/method/body/rate limits, query-token auth в upstream | -| exact `/bitrix/sync/webhook/alert` | `bitrix-sync:8080` | public HTTPS smart-process event; те же ограничения | - -Notification paths внутри `/api/` ВМ1 имеют отдельные edge-зоны. На обоих public hosts `/internal/`, `/_internal/`, Redis/OTLP/admin/status/config files возвращают `404`; fallback на другую VM или SPA запрещён. До full sync cutover оба exact webhook route ВМ2 закрыты либо возвращают retryable `503`; успешный `2xx ignored` запрещён. - -Query не участвует в exact location matching: URL штатного робота `/bitrix/sync/webhook/?token=...&ID=...` попадает в соответствующий exact route. До proxy nginx проверяет непосредственный source IP по version-controlled `BITRIX_WEBHOOK_ALLOWED_CIDRS`; пустой/невалидный список при enabled receiver блокирует deployment. Адрес из недоверенного `X-Forwarded-For` не используется. При внешнем LB сначала настраиваются его trusted CIDR и нормализация real IP. - -`BITRIX_SYNC_ENABLED`, public route, readiness и allow-list согласуются одним -preflight: disabled требует `deny all;`, enabled — reviewed non-empty CIDR и -ready receiver. Обратные комбинации блокируют deployment. - -Запрос вне allow-list получает generic `403` без proxy. В безопасном журнале с ограниченным retention сохраняются только timestamp, source IP, route class и outcome; query/body не сохраняются. Telemetry pipeline экспортирует `webhook_rejected_total{receiver,reason="source_ip"}` без IP label. Allow-list не расширяется автоматически: всплеск Contact, восстановленных инкрементальной reconciliation, инициирует проверку rejected-IP журнала, подтверждение принадлежности адреса Битрикс24 и reviewed reload конфигурации. - -## 3. Upstreams - -Именованные upstream ВМ1: `api_backend`, `keycloak`, `sms_service`, `bitrix_local`, опционально `frontend_dev`, а также private `processing_gateway` только для вызовов Message Safety из api-backend. - -Nginx ВМ2 имеет независимые server blocks: - -- public `80/443` на отдельном DNS host: ACME/redirect и два exact CRM webhook; -- private `8443` с сертификатом internal CA: только server-to-server Message Safety и approved ops. - -Private `8443` также fail-closed: до утверждённого Safety cutover active -caller allow-list содержит только `deny all;`; после cutover он совпадает с -SG/host-firewall источниками ВМ1. Расхождение любого из трёх слоёв блокирует -rollout. - -| Path | Local upstream | Caller | -|---|---|---| -| `/internal/safety/v2/*` | `message-safety-api:8080` | api-backend ВМ1 | -| `/internal/sync/v1/*` | `bitrix-sync:8080` | ops/allow-listed service | - -Public и private server blocks не имеют общего fallback. Все прочие paths/methods возвращают `404/405`. Private listener доверяет forwarded headers только от allow-listed private caller; public listener применяет собственную trusted proxy policy. - -Upstream failures не перенаправляются на другой сервис и не попадают в SPA. API возвращает `502/504` с безопасным nginx body и `X-Request-ID`; custom JSON error допустим для `/api`, но не имитирует backend domain code. - -## 4. HTTP/HTTPS и TLS - -- public host каждой VM на `:80` обслуживает только ACME challenge и `308 https://$host$request_uri`; исключение — `/bitrix/sync/webhook/contact|alert`, которые возвращают generic `404/426` без redirect и отражения query token; -- выделенный API host, если появится, не имеет listener `:80`; -- `:443 ssl http2`, TLS 1.2/1.3, современные cipher suites, session tickets по ops policy; -- сертификат доверенного CA, private key read-only и недоступен приложению; -- OCSP stapling при поддержке CA/DNS; -- HSTS включается только после успешной проверки HTTPS: `max-age` из env, затем по решению ops `includeSubDomains`; preload не включать автоматически; -- OIDC redirects, cookies и external URLs всегда HTTPS. - -## 5. ACME lifecycle - -Выбран webroot Certbot/ACME client с общими named volumes: - -```text -nginx-certs -> /etc/letsencrypt (rw у certbot, ro у nginx) -nginx-acme-webroot -> /var/www/certbot -frontend-static -> /usr/share/nginx/html:ro -``` - -Bootstrap: - -1. DNS указывает на VM; 80/443 разрешены. -2. Запустить временный HTTP config с `/.well-known/acme-challenge/`. -3. Выпустить certificate без остановки nginx. -4. Проверить `nginx -t`, атомарно активировать TLS config, reload. - -Renew container/host timer выполняет `certbot renew` минимум дважды в сутки; -после фактического renewal проверяет рабочую конфигурацию командой -`docker compose exec -T nginx nginx -t -c /tmp/nginx.conf` и отправляет -master-процессу `docker compose kill -s HUP nginx`. Bare-команды `nginx -t` и -`nginx -s reload` запрещены: контейнер read-only, а рабочие config/PID находятся -в `/tmp`. При ошибке остаётся старый worker/config/cert и срабатывает alert. -Успешный deploy/renew hook возвращает `0` с пустым stderr: вывод успешного -`nginx -t` и progress signal command перехватывается или подавляется; при -ошибке сохранённая диагностика полностью печатается в stderr. Любой stderr на -success path считается дефектом интеграции с Certbot. -Контролируются expiry days и последняя успешная попытка. Staging CA используется -в rehearsal, чтобы не исчерпать лимиты. - -## 6. Request ID и forwarded headers - -На edge формируется trusted request id. Базовый nginx не генерирует UUID штатной переменной, поэтому используется njs/Lua либо модуль request-id, включённый в закреплённый image. Входящий `X-Request-ID` принимается только если соответствует UUID/ULID и длине; иначе генерируется новый. - -Upstream получает: - -```text -Host: original host -X-Real-IP: trusted real client IP -X-Forwarded-For: normalized proxy chain -X-Forwarded-Proto: https -X-Forwarded-Host: original host -X-Forwarded-Port: 443 -X-Request-ID: edge request id -traceparent: входной валидный либо новый согласно OTEL integration -``` - -Ответ всегда содержит `X-Request-ID`. Клиентские `X-Forwarded-*` от недоверенного адреса перезаписываются. `Authorization`, `Cookie`, query string и body не попадают в access log. - -## 7. WebSocket - -Только exact `location = /api/v1/realtime`: - -- `proxy_http_version 1.1`; -- `Upgrade $http_upgrade`, `Connection` через `map`; -- buffering и response cache выключены; -- read timeout больше ping interval (ориентир 75 с), send timeout bounded; -- rate limit handshake и `limit_conn` на IP; -- query `access_token` вырезается/редактируется из логов; -- subprotocol передаётся; -- при shutdown nginx позволяет grace reconnect, frontend восстанавливается polling. - -## 8. Timeouts и body limits - -Общие ориентиры: - -| Группа | connect/send/read | -|---|---| -| обычный API | 3s / 30s / 30s | -| auth | 3s / 30s / 60s | -| Bitrix callback | 3s / 30s / 60s | -| Direct SMS callback | 3s / 30s / 60s | -| WS | 3s / 30s / 75s+ | -| message POST | 3s / 30s / `MESSAGE_SAFETY_TASK_POLL_MAX_SEC + 30s` минимум | - -При default safety max 300 сек message read timeout не меньше 330 сек. Значение генерируется из env template до startup; nginx не выполняет арифметику env runtime. - -`client_max_body_size` global 8m по arch-04, но JSON API locations получают более строгие limits, где возможно. Байты вложения не проходят через nginx/API: клиент PUT напрямую в S3. Buffering request допустим для малого JSON; для callback устанавливается bounded temp storage. Header count/size ограничены. - -## 9. Edge rate limits - -`limit_req_zone` использует binary remote address и отдельные зоны: - -- `auth`: `NGINX_RATE_LIMIT_AUTH`, малый burst, без большого nodelay; -- `public`: config/content, 60/min/IP; -- `api`: общий API; -- `polling`: GET messages fallback; -- `downloads`: issuance URL; -- `notifications_read`: list/counter/detail; -- `notifications_action`: read/hide/CTA/button; -- `notification_upload`: универсальные upload drafts; -- `notifications_public`: guest notifications и каталог видов; -- `bitrix_callbacks`: мягкий burst для повторов; -- `idgtl_callbacks`: отдельный bounded burst, учитывающий повтор каждые 5 минут в течение суток; -- `ws_connect`: handshake; -- `connections`: `limit_conn`. - -Ответ превышения — `429`, `Retry-After` (статический/вычисляемый для зоны) и request id. Edge не реализует user-level бизнес-лимит; это делает API/Redis. OPTIONS не должен расходовать auth budget чрезмерно. Bitrix webhook retries имеют отдельный достаточный burst, проходят source IP allow-list и проверяют query receiver token в сервисе. - -## 10. Static SPA и dev mode - -Production: - -- root `${FRONTEND_STATIC_PATH}`; -- существующие hashed assets — `Cache-Control: public, max-age=31536000, immutable`; -- `index.html`, manifest/service worker — `no-cache` либо короткая revalidation; -- `try_files $uri $uri/ /index.html`; -- dotfiles, source maps (если не предназначены), config/env artifacts запрещены; -- API/Bitrix/auth/internal locations объявлены до SPA и никогда в неё не fallback. - -Dev: при `FRONTEND_DEV_PROXY_ENABLED=true` `/` проксируется на allow-listed `EXPO_DEV_SERVER_URL`, с WS/HMR. Этот режим запрещён при `APP_ENV=production-like|production`; startup template validator fail-fast. - -## 11. Public caching - -`GET /api/v1/public/app-config` и `/content` кэшируются только для GET/HEAD, с key `scheme+host+uri+accept-encoding` (и locale query, если контракт его использует). Backend `Cache-Control`/ETag учитываются. Базовый TTL — `security.public_cache.max_age_seconds`/3600. - -- `Set-Cookie` не кэшируется; -- Authorization request bypass cache; -- stale-if-error допускается ограниченно и маркируется `Warning`; -- mutation, auth, Bitrix, profile, dialogs, downloads и errors не кэшируются; -- cache status пишется в log, но наружу технологический header опционален. - -## 12. CORS, CSP и security headers - -CORS — exact allow-list из согласованного deploy config; application CORS остаётся последней инстанцией. Wildcard с credentials запрещён. Allowed headers: `Authorization`, `Content-Type`, `X-Request-ID`, `X-Ux-Session-Id`, `Idempotency-Key`, `traceparent`; методы соответствуют OpenAPI. Preflight получает bounded max-age. - -Для SPA: - -- CSP default-src `'self'`; -- connect-src `'self'` `https:` к разрешённому S3 endpoint и `wss:` текущего host; -- img-src `'self' data: blob:` и разрешённые signed HTTPS resources; -- object-src `'none'`, base-uri `'self'`, frame-ancestors `'none'`; -- frame-src `'none'`: инструкция `install_app` всегда открывается в новой вкладке, iframe/модалка не поддерживается; -- script-src без `unsafe-eval` production; nonce/hash при необходимости; -- style-src policy согласовать с Expo build, постепенно исключить unsafe-inline. - -Также: `X-Content-Type-Options: nosniff`, `Referrer-Policy: strict-origin-when-cross-origin`, `Permissions-Policy`, frame protection через CSP, корректный COOP/CORP без поломки Keycloak redirect/S3. `Server` tokens скрыты; upstream `X-Powered-By` удаляется. - -Bitrix placement может требовать embedding: для exact `/bitrix/placement` CSP `frame-ancestors` задаётся отдельным allow-list Bitrix24, а не ослабляет SPA. - -### Callback i-Digital Direct - -- Только exact `location = /callbacks/idgtl/sms`; разрешён только `POST`, остальные методы отклоняются. -- Source IP allowlist — `185.203.96.7`, но значение обязательно повторно сверяется с актуальной документацией Direct перед production. При WAF/LB используется только нормализованный trusted client IP. -- TLS обязателен; cache выключен; body size ограничен под массив callback items. -- Basic `Authorization` передаётся `sms-service`, но никогда не записывается в access/error logs. URL с credentials также редактируется. -- Nginx не проверяет provider payload и не преобразует статусы; это делает `sms-service`. Ошибку upstream/DB нельзя маскировать `2xx`, иначе Direct не повторит callback. - -## 13. Health - -- внутренний `GET /nginx-health/live` возвращает static 200 и доступен Docker healthcheck; -- внешний health публикуется только если нужен мониторингу, с allow-list; -- nginx health не утверждает готовность upstream; -- Docker healthcheck использует только binary, гарантированно присутствующий - и проверенный внутри exact pinned nginx digest; `wget`/`curl` запрещены, если - их наличие не подтверждено image inventory; -- внешняя synthetic проверка отдельно проверяет TLS, redirect, public API, auth discovery и callback route; -- upstream `/health/ready` не агрегируется публично без решения ops. - -## 14. Логи и OTEL correlation - -JSON access log: timestamp, request_id, trace_id (если извлечён), remote IP/hashed policy, host, method, route class, normalized URI на основе `$uri` без `$request_uri`/`$args`, status, bytes, duration, upstream addr/status/time, cache status, TLS protocol/cipher, user agent при принятой retention. - -Не логируются Authorization, cookies, request/response body, OTP, tokens, query token, presigned query, PII. Error log структурирован настолько, насколько позволяет nginx; debug выключен production. - -Nginx передаёт W3C trace context; native OTEL module допустим при закреплённой версии. Если edge создаёт span, request id остаётся отдельным correlation key. Логи идут stdout/stderr; Docker/collector отвечает за доставку и rotation. - -## 15. Layout конфигурации - -```text -nginx/ - Dockerfile - docker-compose.yml - nginx.conf - templates/ - 00-maps.conf.template - 10-upstreams.conf.template - 20-http-redirect.conf.template - 30-https-site.conf.template - snippets/ - proxy-common.conf - security-headers.conf - tls.conf - rate-limits.conf - websocket.conf - njs/request_id.js - scripts/{render,validate,reload-after-renew}.sh - tests/ -``` - -Image и modules pin по digest/version. Render использует allow-list env и fail-fast для пустых host/cert/upstream/timeouts. Секреты в rendered config не требуются. - -## 16. Docker Compose - -`nginx` подключён к `public` и `backend`, публикует `${NGINX_HTTP_PORT}:80`, `${NGINX_HTTPS_PORT}:443`; filesystem read-only, tmpfs для cache/run/temp, non-root где позволяет bind ports/capabilities. Cert/static volumes read-only. ACME client имеет только необходимые volumes/network. - -`depends_on` health не заменяет retry/readiness. После healthy upstream -обязателен config test с production service DNS names и reload/recreate nginx. -После recreate upstream повторяется reload policy либо используется явно -протестированный dynamic resolver. Nginx может временно отдавать bounded 502, -но не считается ready до этой post-ready проверки. Resource/FD limits -учитывают WS. - -## 17. Failure behavior - -- API/Keycloak upstream down: bounded 502/504, без SPA fallback. -- Safety slow: nginx ждёт message budget ≥ max+30, затем 504; backend checkpoint продолжает recovery. -- Redis down не влияет на запуск nginx; app решает degraded policy. -- cert renewal failed: текущий cert продолжает работу, alert до expiry. -- invalid new config: reload отменяется, старые workers остаются. -- disk/cache full: public cache bypass/evict, requests продолжаются где безопасно. -- DNS upstream changed: resolver/restart policy восстанавливает адрес. -- overload: 429/503 на edge, bounded queues; не накапливать неограниченные connections. - -## 18. Валидация и тесты - -Команды acceptance: - -```text -docker compose config -docker compose exec nginx nginx -t -curl -I http://tohin.ru/ -curl -vk https://tohin.ru/api/v1/public/app-config -openssl s_client -connect tohin.ru:443 -servername tohin.ru -curl -i https://tohin.ru/internal/safety/v2/messages/check -``` - -Автоматические тесты: - -- Test::Nginx/containers для route precedence, methods, 404 internal; -- TLS scan: только 1.2/1.3, chain/hostname/expiry; -- redirect и ACME challenge; -- request-id valid/invalid/generation/propagation; -- forwarded spoof rejection; -- WS handshake, ping idle и reconnect; -- message request длительнее safety max не обрывается до budget; -- body/header limits; -- rate zones/429/Retry-After; -- public cache HIT/MISS/bypass/no private cache; -- CSP/CORS preflight и Bitrix placement exception; -- upstream down/timeout, failed reload, renewal rehearsal; -- logs не содержат secrets/query tokens. -- CRM webhook exact routes принимают query без изменения location matching; allowed source IP проксируется, wrong IP получает `403` до upstream; -- HTTP webhook URL с query token не перенаправляется на HTTPS и не отражает query в `Location`/error; -- source-IP rejects попадают в безопасный bounded-retention журнал и low-cardinality telemetry без query/body/IP label; -- allowed Direct callback проходит; wrong IP/method и любой `/internal/sms/*` отклоняются; Authorization отсутствует в логах. -- `/internal/notifications/*` снаружи всегда `404`; notification read/action/upload/public routes используют свои зоны и возвращают `429`. -- CSP содержит `frame-src 'none'`; инструкция проверяется как новая вкладка без embedded content. - -## 19. Definition of Done - -- на каждой VM ровно один nginx; ВМ1 и ВМ2 независимо публикуют только свои утверждённые `80/443`, ВМ2 дополнительно слушает private `8443`; -- TLS/ACME bootstrap, renewal и safe reload испытаны; -- routing matrix и route precedence покрыты; -- CRM webhook достигает ВМ2 напрямую и продолжает приниматься при остановленном nginx ВМ1; -- CRM webhook ограничен version-controlled source IP CIDR allow-list; query token и form body отсутствуют в access/error logs и traces; -- internal endpoints/ports извне недоступны; -- WS работает на `/api/v1/realtime`; -- message timeout равен safety max + минимум 30 секунд; -- request id и trusted forwarded headers корректны; -- limits, public cache, CSP/CORS/security headers проверены; -- static production и dev proxy guard работают; -- JSON logs коррелируют request/trace и не содержат секретов; -- health/synthetic checks и failure tests проходят; -- image non-root/read-only насколько возможно, versions pinned; -- runbooks для cert, reload, upstream outage и rollback готовы. - -## 20. Решения, допущения и TBD - -**Решения:** независимые public nginx ВМ1/ВМ2; CRM webhook приходит прямо на ВМ2 через source IP CIDR allow-list; private `8443` ВМ2 используется только server-to-server; njs/module для UUID; public internal paths → 404; webroot ACME; отдельная CSP для Bitrix placement; public cache только allow-listed endpoints. - -**Допущения:** ВМ1 и ВМ2 используют разные public hosts и сертификаты; upstream service names стабильны внутри каждого Compose; S3 CORS настраивается отдельно. - -**TBD:** N1 доверенные WAF CIDR; N2 production cipher suite/OCSP; N3 нужен ли публичный health; N4 точный CSP Expo build; N5 Bitrix frame ancestor domains; N6 финальные burst/connection limits; N7 certbot vs другой ACME client после ops review. diff --git a/modules/module-04-redis.md b/modules/module-04-redis.md deleted file mode 100644 index 2294958..0000000 --- a/modules/module-04-redis.md +++ /dev/null @@ -1,289 +0,0 @@ -# module-04. Проектная спецификация Redis - -> Статус: целевая спецификация Redis для двух Compose-контуров; legacy DB2 stub описан только до cutover. -> Источники: [`README.md`](README.md), [`arch-00-glossary.md`](arch-00-glossary.md), [`arch-01-system-architecture.md`](arch-01-system-architecture.md), [`arch-02-api-contracts.md`](arch-02-api-contracts.md), [`arch-03-docker-compose-blueprint.md`](arch-03-docker-compose-blueprint.md), [`arch-04-settings-and-content.md`](arch-04-settings-and-content.md), [`arch-05-agent-development-process.md`](arch-05-agent-development-process.md), [`module-01-api-backend.md`](module-01-api-backend.md). - -## 1. Назначение и инварианты - -Redis разделён по deployment/security boundary: - -- Redis ВМ1: DB0 (`api-backend` idempotency/rate) и DB1 (realtime/coordination); -- Redis Safety ВМ2: отдельный instance для hot cache, rate limiting и optional worker wake-up; -- legacy DB2 ВМ1 существует только для test stub v1 до cutover и после него удаляется. - -Redis не является бизнес-очередью, source of truth сообщений, sync tasks, audit, профилей или delivery checkpoint. Надёжные состояния остаются в managed PostgreSQL/S3. Потеря Redis может ухудшить сервис, но не должна создавать потерю подтверждённых сообщений либо дубль side effect: durable idempotency/outbox/checkpoint api-backend описаны в module-01. - -OTP counters api-backend в Redis не хранит; они принадлежат Keycloak/SPI. - -## 2. Версия и topology - -Redis 7.x, image закреплён по digest. На каждой VM одна нужная primary instance без replica/Sentinel в MVP. Клиенты используют connection pool, bounded timeouts и не выполняют опасные команды. - -Logical DB — изоляция имён, не security boundary. Safety уже вынесен в отдельный instance ВМ2; DB0/DB1 остаются на ВМ1. - -## 3. Общие правила ключей - -Формат: `han:{domain}:{purpose}:{hashed-or-public-id}:{version}`. Только ASCII lowercase separators. Public UUID допустим; IP, phone, email, token, text и filename — только HMAC/SHA-256 с server-side pepper там, где нужна защита dictionary attack. - -- key length желательно ≤ 200 bytes; -- значения versioned (`v=1`); -- timestamps — Unix ms/seconds или RFC3339, формат фиксирован для каждого key; -- wildcard `KEYS` production запрещён; только `SCAN` для ops; -- каждый non-channel key имеет TTL, кроме явно обоснованных bounded structures; -- large payload/presigned URL/token/message text запрещены. - -## 4. DB0: API rate limiting - -Примеры: - -| Key | Тип/value | TTL | -|---|---|---| -| `han:api:rl:user:{user_id}:{route_hash}:{window}` | ZSET timestamps либо counter | window + jitter | -| `han:api:rl:ip:{ip_hmac}:{route_hash}:{window}` | ZSET/counter | window + jitter | -| `han:api:rl:dialog:{dialog_id}:message:{window}` | ZSET/counter | window + jitter | -| `han:api:rl:service:{service}:{route_hash}:{window}` | counter/token bucket | window + jitter | - -Алгоритм — atomic Lua/function: удалить старые entries, посчитать, добавить текущий request, установить expiry, вернуть `allowed`, `remaining`, `retry_after_ms`, `reset_at`. Для fixed window `INCR` и первый `EXPIRE` выполняются в одном script, чтобы не оставить бессрочный key. - -Clock используется Redis `TIME` внутри script, а не client wall clock. Script загружается при startup, SHA кэшируется; после `NOSCRIPT` выполняется контролируемый reload. Route labels — bounded allow-list/hash, исключающий cardinality attack. - -## 5. DB0: idempotency - -| Key | Значение | TTL | -|---|---|---| -| `han:api:idem:{scope}:{user_id}:{key_hmac}` | HASH/MessagePack: state, fingerprint, status, sanitized response, resource id, version | 24h | -| `han:api:idemlock:{scope}:{user_id}:{key_hmac}` | random owner token | 30s + heartbeat | - -State transitions `absent → in_progress → completed`; fingerprint mismatch возвращает conflict. Создание/сравнение/lock выполняется Lua. Unlock/extend разрешены только если owner token совпадает (`compare-and-delete/expire` script). - -Response не содержит tokens, cookies, presigned URL или PII. Transient 503/504 не фиксируется как окончательный completed. PostgreSQL `idempotency_records` — durable fallback; Redis — ускоритель. При cache loss API читает durable row и прогревает key. - -## 6. DB1: realtime - -| Key/channel | Формат | TTL | -|---|---|---| -| `han:rt:conn:{connection_id}` | HASH: user_id, instance, last_seen, subscriptions_count | 90s | -| `han:rt:user:{user_id}:connections` | ZSET connection_id → heartbeat | 120s | -| `han:rt:dialog:{dialog_id}` | Pub/Sub channel | нет хранения | -| `han:rt:user:{user_id}` | Pub/Sub channel | нет хранения | - -Heartbeat атомарно обновляет connection и membership; cleanup удаляет stale ZSET entries bounded batches. Pub/Sub — at-most-once notification. Payload содержит только event id/type/entity UUID и DTO, допустимый realtime контрактом; DB остаётся source of truth. После reconnect frontend всегда делает REST reconciliation. - -Redis Streams не используются как бизнес queue. Если позже понадобится durable realtime replay, сначала меняется архитектура и выбирается PostgreSQL outbox/event broker. - -## 7. DB1: coordination locks - -| Key | TTL | -|---|---| -| `han:coord:lock:safety-recovery:{task_id}` | 30s | -| `han:coord:lock:delivery:{message_id}` | 30s | -| `han:coord:lock:settings-refresh:{instance}` | 30s | - -Acquire: `SET key owner NX PX ttl`; extend/release — Lua compare owner. Worker обязан опираться также на PostgreSQL row lease/`FOR UPDATE SKIP LOCKED`; Redis lock — оптимизация, не единственная защита. Fencing token рекомендуется для внешнего side effect, а уникальные DB constraints/idempotency остаются финальной защитой. - -## 8. Redis Safety ВМ2 - -| Key | Тип/value | TTL | -|---|---|---| -| `han:safety:rl:service:{caller}:{window}` | counter | window+jitter | -| `han:safety:text:{analysis_hash}:{rules_version}` | hot text-rules result, monitor rule ids без raw text | active config, seed ≤48h | -| `han:safety:verdict:{content_hash}:{config_version}:{detector_bundle}` | hot file verdict cache | active config, seed ≤30d | -| `han:safety:link:{url_hash}:{rules_version}:{config_version}` | stable local policy cache | active config, seed ≤48h | -| `han:safety:dns:{host_hash}:{rrtype}` | DNS answer; classification повторяется под текущей policy | actual TTL, active hard max seed 900s | -| `han:safety:wakeup` | Pub/Sub notification only | no storage | - -PostgreSQL `message_safety.safety_tasks` — единственный queue/lease source (`FOR UPDATE SKIP LOCKED`, fencing generation). Redis не хранит authoritative task state, locks или leases. Cache loss/restart безопасно восстанавливается из PostgreSQL; Redis outage не выключает core Safety. - -Legacy v1 stub может временно использовать DB2 ВМ1 для random task state. Этот namespace не используется production v2 и удаляется вместе со stub. - -## 9. Serialization и limits - -- простые counters — integer; -- locks — opaque random 128-bit token; -- metadata — Redis HASH либо компактный JSON с `schema_version`; -- max value target 32 KiB, hard application guard 128 KiB; -- response cache хранит только allow-listed sanitized JSON; -- decode error считается cache miss, key удаляется/карантинируется и поднимается metric. - -## 10. TTL policy - -| Категория | TTL | -|---|---| -| idempotency completed | 24h по arch-02 | -| idempotency in-progress lock | 30s, heartbeat bounded | -| rate limit | window + 10–30% deterministic jitter | -| realtime connection | 90s; set membership 120s | -| coordination lock | 30s | -| safety file hot cache | ≤30d; authoritative row/version в PostgreSQL | -| safety text-rules cache | 48h; invalidation by rules version | -| safety stable link policy cache | 48h | -| safety DNS cache | actual DNS TTL, hard max 900s | - -Новый key без TTL запрещён contract test, кроме Pub/Sub channel (не key) и ops metadata с явным обоснованием. - -## 11. Atomicity и Lua governance - -Scripts/functions хранятся в репозитории рядом с клиентом, versioned и тестируются на real Redis. Запрещены unbounded loops/SCAN внутри Lua. Входные массивы ограничены. Script timeout отслеживается; `SCRIPT KILL` runbook применяется только если нет writes либо после оценки. - -Обязательные scripts: - -- rate-limit evaluate; -- idempotency reserve/complete/conflict; -- lock release/extend; -- realtime heartbeat/cleanup membership; -- safety task get+increment poll при необходимости. - -Redis transaction не координирует PostgreSQL/S3/HTTP. Cross-system consistency обеспечивается DB checkpoint/outbox и idempotent finalize. - -## 12. Persistence - -Решение MVP: AOF `appendonly yes`, `appendfsync everysec` плюс RDB snapshots (`save 900 1`, `300 100`, `60 10000` либо tuned). Это ускоряет восстановление ephemeral state, но не превращает Redis в authoritative store. - -`aof-use-rdb-preamble yes`, automatic rewrite с порогами; volume `redis-data`. При corruption используется `redis-check-aof`/restore clean instance, а сервисы восстанавливают authoritative state из PostgreSQL. - -RPO Redis до ~1 секунды приемлем, потому что бизнес-RPO задаётся PostgreSQL/S3. Backup Redis не обязателен для бизнес-восстановления, но периодическая копия RDB/AOF полезна для ops forensic без secrets. - -## 13. Memory и eviction - -`maxmemory` задаётся относительно container limit (ориентир 70–75%, оставляя overhead/fork). Начальная оценка для одной VM — 512 MiB, уточняется load test. - -Eviction MVP: `volatile-lru`/`volatile-ttl`, так как все application keys имеют TTL. `allkeys-lru` опасен для idempotency при memory pressure; `noeviction` может полностью закрыть writes. Окончательный выбор после нагрузки: предпочтительно `volatile-lru` + alerts, а при разделении instances DB0 idempotency получает отдельную noeviction policy. - -Контролируются `used_memory`, RSS, fragmentation, evicted_keys, expired_keys, key count/avg TTL по DB. OOM/eviction idempotency не создаёт дубль благодаря PostgreSQL fallback. - -## 14. Sizing - -Расчёт до production: - -```text -DB0 rate = peak identities × routes × active windows × bytes/key -DB0 idem = mutating requests/24h × avg sanitized record -DB1 = peak connections × connection metadata + Pub/Sub buffers -Redis Safety = hot verdict/link/DNS entries + rate windows + Pub/Sub buffers -each instance total × 1.5 allocator/fragmentation × 1.3 growth reserve -``` - -Pub/Sub output buffers и slow consumers имеют hard/soft limits. Load test фиксирует peak RPS, WS connections, idempotency response size и AOF rewrite headroom. - -## 15. Auth, ACL и network boundary - -Оба Redis не публикуют `6379` на host и подключены только к local Docker `backend` своей VM. `protected-mode yes`, default user отключён. ACL users: - -- `api_backend`: DB0/DB1 key prefixes, нужные command categories; -- `message_safety`: только Redis Safety prefixes; -- `ops_health`: `PING`, ограниченный `INFO`; - -Важно: Redis ACL не ограничивает logical DB напрямую надёжно; key-prefix patterns и разные credentials обязательны. `SELECT` запрещается, клиент URL сразу задаёт DB, но ACL prefix остаётся основной защитой. - -Dangerous/admin commands (`FLUSHALL`, `FLUSHDB`, `CONFIG`, `MODULE`, broad KEYS`, replication changes) запрещены application users; rename-command не считается основной защитой. - -Пароли сильные, только env/secret mount, rotation current/new через rolling deploy. Внутри одной VM TLS Redis опционален при закрытой Docker network; при выносе за host/VPC TLS обязателен (`rediss://`) и plaintext отключается. - -## 16. Docker/runtime - -```text -redis/ - docker-compose.yml - redis.conf - users.acl.template - scripts/ - tests/ -``` - -Compose: pinned Redis image, `expose: 6379`, без `ports`, `backend` network, `redis-data:/data`, config/ACL read-only, non-root UID, no-new-privileges, dropped capabilities, resource/memory/ulimit settings. - -Startup валидирует config и ACL, permissions volume, затем Redis. Healthcheck использует ACL health user и `redis-cli --no-auth-warning PING`, secret не печатается. Graceful stop timeout позволяет AOF flush. - -URL: - -```text -REDIS_URL=redis://api_backend:@redis:6379/0 -REDIS_REALTIME_URL=redis://api_backend:@redis:6379/1 -MESSAGE_SAFETY_REDIS_URL=redis://message_safety:@redis:6379/0 -``` - -Первые два URL существуют только на ВМ1. На ВМ2 `MESSAGE_SAFETY_REDIS_URL=redis://message_safety:@redis:6379/0`; credential доставляется secret file и не входит в общий `.env`. - -## 17. Health и degraded behavior - -`PING` проверяет liveness Redis; readiness приложений проверяет auth, correct DB и выполнение малого read/write/expire script без оставления key. - -При Redis недоступен: - -- message send, attachment init и download URL api-backend fail-closed `503`, если нельзя безопасно применить лимит/idempotency; -- completed idempotency восстанавливается из PostgreSQL; -- profile/history GET могут работать под edge limits; -- public GET использует bounded local conservative limiter/cache; -- realtime cross-instance publish/coordination деградирует; REST/polling остаётся source of truth; -- production Safety продолжает task claim/poll через PostgreSQL; hot cache/rate/wakeup деградируют и прогреваются после восстановления Redis; -- legacy stub v1 может стать недоступным при потере своей DB2 до cutover; -- internal inbox не теряется из-за Redis, так как durable receipt в PostgreSQL. - -При latency выше threshold clients используют short timeout/circuit, не создают бесконечные retry storms. Reconnect — exponential backoff+jitter. - -## 18. Backup и restore - -Redis backup не используется для бизнес restore. Runbook: - -1. остановить/изолировать corrupted instance; -2. при целостном AOF/RDB восстановить на отдельном instance и проверить; -3. иначе поднять пустой Redis; -4. api-backend прогревает idempotency по durable records, realtime восстанавливается reconnect/polling; -5. production safety tasks продолжают обрабатываться из PostgreSQL; Redis Safety прогревается лениво. - -Не копировать Redis dump в небезопасное место: keys содержат UUID и hashed identifiers. - -## 19. Metrics и alerts - -- availability, commands/sec, latency percentiles; -- connected/blocked clients, rejected connections; -- memory/RSS/fragmentation, maxmemory ratio; -- evictions/expirations/keyspace hits/misses; -- AOF fsync latency/rewrite status/last save; -- replication metrics зарезервированы; -- key count/avg TTL по DB без key values; -- script errors/NOSCRIPT/slowlog; -- rate limit decisions, idempotency hit/conflict/fallback; -- Pub/Sub subscribers/output buffer/slow disconnect; -- Safety hot-cache hit/miss, DNS TTL cap и wakeup subscribers. - -Alerts: unavailable, p99 latency, >80/90% memory, any sustained evictions, AOF error, no recent persistence, client buffer pressure, unexpected keys without TTL. - -## 20. Тесты - -- ACL: каждый service видит только свой prefix/commands; -- порт 6379 недоступен с host/public network; -- rate Lua concurrency и exact Retry-After; -- idempotency same/different fingerprint, lock ownership, expiry, Redis loss + PostgreSQL fallback; -- realtime heartbeat cleanup, duplicate disconnect, Pub/Sub loss + REST recovery; -- locks expiry/late owner/fencing; -- Safety cache loss/rebuild, DNS TTL cap и доказательство отсутствия task/lease state в Redis; -- `NOSCRIPT` reload; -- all application keys имеют TTL; -- max value/invalid serialization; -- restart with AOF/RDB, corrupted AOF rehearsal, empty restore; -- memory pressure/eviction и no duplicate business side effect; -- network partition, latency, reconnect backoff; -- logs/metrics не содержат secret/value/PII. - -## 21. Definition of Done - -- DB0/DB1/DB2 roles и prefixes реализованы; -- Lua scripts atomic, bounded, versioned и покрыты real Redis tests; -- idempotency 24h и durable fallback доказаны; -- realtime loss восстанавливается REST; -- Safety DB2 task TTL превышает poll/recovery budget; -- AOF/RDB, volume, restart и clean-instance recovery проверены; -- maxmemory/eviction/resource limits основаны на load test; -- ACL users и network isolation работают, порт не published; -- health/degraded policies реализованы в clients; -- dashboards/alerts/runbook готовы; -- Redis не используется как `sync_queue`, delivery queue, message/audit source of truth или OTP store. - -## 22. Решения, допущения и TBD - -**Решения:** один instance/три DB MVP; AOF everysec + RDB; Pub/Sub best effort; PostgreSQL durable fallback; prefix ACL; все application keys с TTL. - -**Допущения:** по одной Redis instance на ВМ1/ВМ2 и одна Safety API replica на старте; Redis loss допустим без потери business truth. - -**TBD:** R1 точный maxmemory после load profile; R2 eviction policy после измерений; R3 credential env names в arch-04; R4 Safety task TTL/recovery margin; R5 TLS при изменении network topology; R6 момент разделения DB на instances; R7 RPO/RTO ops target. diff --git a/modules/module-10-deployment-runbook.md b/modules/module-10-deployment-runbook.md deleted file mode 100644 index c2b9d7c..0000000 --- a/modules/module-10-deployment-runbook.md +++ /dev/null @@ -1,1421 +0,0 @@ -# module-10. Runbook развёртывания HAN Chat - -> Статус: целевой runbook ВМ1/ВМ2. Команды существующего stub-контура применимы только до production Safety cutover и явно отмечены как legacy. -> Все значения в `<УГЛОВЫХ_СКОБКАХ>` — placeholders. Команды с `cd ` требуют подстановки реального пути корня backend-репозитория на VM. -> Канонические источники: [`../architectory/README.md`](../architectory/README.md), [`../architectory/arch-00-glossary.md`](../architectory/arch-00-glossary.md), [`../architectory/arch-01-system-architecture.md`](../architectory/arch-01-system-architecture.md), [`../architectory/arch-02-api-contracts.md`](../architectory/arch-02-api-contracts.md), [`../architectory/arch-03-docker-compose-blueprint.md`](../architectory/arch-03-docker-compose-blueprint.md), [`../architectory/arch-04-settings-and-content.md`](../architectory/arch-04-settings-and-content.md), [`../architectory/arch-05-agent-development-process.md`](../architectory/arch-05-agent-development-process.md), [`../architectory/arch-06-service-hosting-security.md`](../architectory/arch-06-service-hosting-security.md), [`module-01-api-backend.md`](module-01-api-backend.md)–[`module-09-observability.md`](module-09-observability.md). - -## 1. Неподвижные правила - -1. Один root Compose project описывается в ``; в steady state его запускает root-owned systemd-unit/helper, а не пользователь из группы `docker`. -2. На каждой VM ровно один nginx; ВМ1 и ВМ2 имеют независимые public DNS/TLS ingress и deployment lifecycle. -3. Application containers не имеют public host ports. Nginx ВМ1 публикует свой `80/443`; nginx ВМ2 — отдельный `80/443` только для exact CRM webhook и private `8443` для Message Safety/internal access. -4. `/internal/*` не маршрутизируется публично. -5. Managed PostgreSQL находится вне Compose, в той же VPC, без public IP. -6. S3 — внешний Selectel-compatible storage; клиент получает только presigned URL. -7. Секреты не коммитятся, не вставляются в команды shell history и не выводятся в отчёты. -8. Миграции выполняются отдельными one-shot steps до новой версии приложения. -9. Message Safety запускается как documented stub до замены; это не production antivirus/moderation. -10. `bitrix-sync` вводится только после выполнения preflight/cutover gates module-07; до этого `BITRIX_SYNC_ENABLED=false`, public webhook закрыт на edge. -11. На ВМ1 и ВМ2 отдельные root Compose projects/systemd units; deploy/rollback выполняются независимо. -11. ВМ2 — самостоятельная service VM с минимальным public webhook ingress, allow-listed egress, отдельным IAM principal и service-specific secret files. -12. OS-роли, SSH/sudo, secrets delivery, container hardening и private-VM lockdown подчиняются arch-06. - -Прямые `docker compose` команды в этом runbook выполняются `admin` только при bootstrap/recovery либо инкапсулируются в утверждённые root-owned systemd-units. Они не являются основанием выдавать `deploy` доступ к Docker daemon. - -## 2. Роли и обозначения - -- **Cloud admin**: VPC, VM, PG, S3, DNS/security groups. -- **Deploy operator / OS user `deploy`**: запуск утверждённых release/rollback/migration systemd-units и root-owned Message Safety mode helper; без группы `docker`, записи в production compose/unit/scripts/config и общего sudo. -- **Break-glass OS user `admin`**: bootstrap и аварийное восстановление; не используется для штатного деплоя. -- **OS user `tunnel`**: только allow-listed local TCP forwarding к private endpoints; без sudo/shell operations. -- **Bitrix admin**: local app, connector, Open Line 8, callbacks. -- **Security owner**: secrets, Keycloak admin MFA, firewall, retention. -- **Safety Service Owner**: API/data contract, capacity result и v2 cutover/rollback sign-off. -- **Rule Pack Owner**: rules bundle, corpus, monitor report и version release. -- **Product Owner**: mnemonic `safety.chat.blocked` и business acceptance chat flow. -- **Operations Owner**: VM2 alerts, ClamAV signatures, incident/reprovision/restore rehearsal. - -Placeholders: - -```text - например chat.example.ru - публичный IPv4 VM - отдельный public host ВМ2, например processing.example.ru - публичный IPv4/LB address ВМ2 - приватный IPv4 VM - например 10.20.0.0/24 - private FQDN/IP managed PG - 5432 или 6432 - han_chat - URL репозитория - /opt/han-chat/backend - immutable tag/git SHA - адрес ops, не placeholder в реальном запуске - разрешённый портал - согласованное в Direct имя отправителя - фактический статический egress IP `sms-worker` - контролируемый номер для provider smoke -``` - -## 3. Stage 0 — решения до provisioning - -### 3.1. Зафиксировать параметры - -- region/availability zone и VPC; -- hostnames и TTL DNS; -- VM image Ubuntu 24.04 LTS; -- sizing; -- PG plan/storage/backups/PITR; -- S3 region/endpoint/bucket names; -- container registry и immutable image tags/digests; -- remote observability backend; -- RPO/RTO и maintenance window; -- ответственных за alerts/Bitrix/Keycloak. -- назначенные Safety Service/Rule Pack/Product/Security/Operations owners и approvals cutover. - -Начальный sizing ВМ2 без local Grafana stack: - -- VM: 4 vCPU, 8 ГБ RAM, 80 ГБ SSD, 4 ГБ swap; -- managed PG: минимум 2 vCPU, 4 ГБ RAM, 50 ГБ, HA по возможности; -- Redis limit: 512 МиБ; -- OTEL Collector: 512 МиБ + 5–10 ГБ queue; -- свободный диск VM после pull/build: не менее 30%. - -Это baseline, не гарантия. До real traffic обязателен load test module-05 §15.4: - -- sustained 10 text checks/s: p95 ≤2 с, p99 ≤5 с; -- sustained 2 file checks/s на 5 worker slots: среднее processing ≤2.5 с, p95 ≤60 с, public wait ≤300 с; -- 100 pending принимаются; 101-й file POST получает retryable `503` без новой task; -- RPS overflow даёт `429 + Retry-After`; -- long Safety poll не блокирует WS/read API; -- если gate не пройден, увеличить slots/CPU/clamd scan lanes и повторить; production traffic не открывать. - -Monthly availability SLO для MVP не задаётся; это не отменяет latency/load gates и alerts. - -### Gate 0 - -- [ ] Владельцы и maintenance window назначены. -- [ ] RPO/RTO приняты хотя бы временно: ориентир RPO PG ≤15 минут/PITR, RTO ≤4 часа. -- [ ] Решено: images pull из registry или build на VM. -- [ ] Remote telemetry backend выбран либо явно принят ограниченный debug-only режим. -- [ ] Риск mock OTP до SMS cutover и Safety stub письменно принят; real SMS не включается без gates module-11. - -**Ожидаемый результат:** есть release checklist с конкретными values; не создано ни одной публичной БД/Redis. - -## 4. Stage 1 — VPC, VM, DNS и security groups - -### 4.1. Сеть - -Создать private subnet для ВМ1, ВМ2, SigNoz и managed PG. ВМ1 и ВМ2 имеют отдельные public IP/LB только для своих nginx; service-to-service и PostgreSQL traffic остаётся private. SSH к обеим VM — только ops VPN/bastion. - -Security groups: - -| Source | Destination | Port | Rule | -|---|---|---:|---| -| trusted ops CIDR/VPN | ВМ1, ВМ2 | SSH `` | allow | -| internet | ВМ1 | TCP 80/443 | allow edge redirect/ACME/application | -| internet | nginx ВМ2 | TCP 80/443 | allow ACME/redirect + exact CRM webhook | -| ВМ1 SG | ВМ2 | TCP 8443 | private TLS only | -| ВМ1/ВМ2 service SG | managed PG | `` | allow по нужным DB roles | -| ВМ2 collector | private SigNoz | TCP 4317 | allow | -| ВМ2 workers | S3 endpoints | TCP 443 | allow | -| ВМ2 `bitrix-sync` | approved Bitrix portal | TCP 443 | allow | -| ВМ2 `freshclam` | approved signature CDN | TCP 443/80 по vendor manifest | allow | -| ВМ2 | trusted DNS/NTP | UDP/TCP 53, UDP 123 | allow | -| internet | managed PG | any | deny | -| internet | ВМ2 | any кроме nginx 80/443 | deny ingress | -| internet | обе VM | 6379, 4317, 4318, 8000, 8080, 8443, 9000 | deny public | - -ВМ2 использует default-deny egress. Registry/OS repositories открываются только в bootstrap/controlled window и затем снова закрываются. Если provider SG не умеет destination allow-list, применяется host firewall/proxy/NAT policy; постоянный open egress для ВМ2 не является допустимым production состоянием. - -### 4.2. DNS - -Создать `A `, `A ` и private DNS `processing.internal → `. Public host ВМ2 используется только CRM webhook; private name не публикуется во внешнем DNS. - -Проверка с рабочей станции: - -```bash -dig +short -dig +short -``` - -Ответ должен совпасть с ``. - -### Gate 1 - -- [ ] PG не имеет public endpoint. -- [ ] SSH доступен только trusted source. -- [ ] Снаружи открыты только 80/443/ограниченный SSH. -- [ ] DNS стабильно разрешается с нескольких resolver. -- [ ] VM достигает private PG и внешних HTTPS endpoints. - -**Ожидаемый результат:** `nc -vz ` с VM успешен; с внешней машины PG недоступен. - -## 5. Stage 2 — hardening Ubuntu и deploy user - -### 5.1. Первичный вход - -Войти cloud user, добавить отдельный deploy key. Не отключать пароль/root до проверки второго SSH-сеанса. - -Прототипный скрипт можно адаптировать: - -```bash -sudo DEPLOY_USER=deploy \ - DEPLOY_DIR=/opt/han-chat \ - SSH_PORT= \ - SWAP_SIZE_GB=4 \ - PUBLIC_DOCKER_PORTS=80,443 \ - ./deploy/setup-vm-han-chat.sh -``` - -Скрипт из [`../../HAN_chat/deploy/setup-vm-han-chat.sh`](../../HAN_chat/deploy/setup-vm-han-chat.sh) полезен для UFW, fail2ban, Docker и `DOCKER-USER`, но перед production: - -- проверить его версию/review; -- не передавать реальные IP/ключи в git; -- проверить auto reboot unattended upgrades относительно maintenance; -- гарантировать, что `deploy` не добавлен в группу `docker` (это root-equivalent); -- оставить `AllowTcpForwarding no` по умолчанию; если нужен DB tunnel, создать отдельного `tunnel` с `AllowTcpForwarding local`, конкретным `PermitOpen`, без sudo/TTY/agent/X11 forwarding; -- установить root-owned systemd-units и `/etc/sudoers.d/deploy` с полными командами и конкретными unit names без wildcard. - -### 5.2. Проверки - -```bash -sudo sshd -t -sudo ufw status verbose -sudo fail2ban-client status sshd -docker version -docker compose version -sudo iptables -L HAN-CHAT-DOCKER -n -v -timedatectl status -df -h -free -h -``` - -Открыть второй SSH session как `deploy`, затем отключить root/password login. Production `.env` позже содержит только non-secret config; runtime secrets доставляются по arch-06. - -### Gate 2 - -- [ ] SSH key login `deploy` проверен во втором сеансе. -- [ ] Root/password auth выключены. -- [ ] `deploy` не состоит в группе `docker`; `sudo -l` содержит только утверждённые конкретные systemd-команды. -- [ ] Production compose, units, deploy scripts и secret mappings принадлежат root и недоступны `deploy` на запись. -- [ ] UFW и DOCKER-USER активны после restart Docker. -- [ ] Для published Docker ports allow rules сопоставляют original host - destination через `conntrack --ctorigdstport`; positive/negative probes - увеличивают counters нужных allow/deny rules после restart Docker и reboot. -- [ ] После обновления firewall helper active `oneshot RemainAfterExit` unit - явно перезапущен; `enable --now` не считается применением новой версии. -- [ ] Docker Engine/Compose plugin закреплены поддерживаемой версией. -- [ ] NTP active; disk/swap соответствуют sizing. -- [ ] Break-glass процедура сохранена вне VM. - -**Ожидаемый результат:** reboot VM не теряет SSH, firewall и Docker service. - -### 5.3. Дополнительный lockdown private/no-egress VM - -Для SigNoz и другой VM, которая после раскатки не должна иметь internet ingress/egress, bootstrap выполняется по lifecycle arch-06. - -До закрытия временного доступа: - -- [ ] SSH разрешён только из trusted ops CIDR и только по ключам. -- [ ] Пакеты/images получены из утверждённых источников, версии/digests зафиксированы. -- [ ] Health/readiness успешны. -- [ ] Проверены необходимые private flows (например app-VM → OTLP/SigNoz). -- [ ] Проверен private путь администрирования через bastion/VPN. -- [ ] Секреты размещены root-owned файлами; временные копии удалены. - -Lockdown: - -- [ ] Public IP удалён, если поддерживается и не нужен. -- [ ] Public SSH и любой internet ingress удалены из cloud SG и host firewall. -- [ ] Общий internet egress закрыт; оставлены только явно утверждённые private flows. -- [ ] С внешней машины SSH и service ports недоступны. -- [ ] С VM не проходит неразрешённый internet egress. -- [ ] Из private network работают SSH и обязательные service flows. -- [ ] Временные bootstrap credentials/rules/files удалены. -- [ ] Результат и время закрытия записаны в release checklist. - -Повторное открытие ingress/egress выполняется только как ограниченная по CIDR/destination и времени break-glass операция. После неё весь lockdown checklist повторяется. - -## 6. Stage 3 — managed PostgreSQL - -### 6.1. Защита и восстановление managed PostgreSQL - -До создания схем включить и проверить настройки managed PostgreSQL: - -- **Daily backup** — автоматический полный снимок БД не реже одного раза в сутки. Нужно задать срок хранения, например 7–14 дней, и убедиться, что резервные копии размещаются отдельно от вычислительного узла БД. Наличие backup без периодической проверки восстановления не считается достаточным. -- **PITR (Point-in-Time Recovery)** — восстановление состояния БД на выбранный момент времени между полными backup за счёт архивации WAL. Это позволяет откатиться, например, к состоянию непосредственно перед ошибочной миграцией или удалением данных. Период доступного восстановления должен соответствовать принятому RPO. -- **Encryption at rest** — шифрование дисков, backup и WAL на стороне провайдера managed PostgreSQL. Ключ управляется провайдером либо KMS организации; пароль пользователя БД не заменяет это шифрование. -- **TLS для соединения с PostgreSQL** — шифрование трафика между контейнерами на VM и managed PostgreSQL с обязательной проверкой имени сервера. CA-сертификат обычно выдаёт провайдер PostgreSQL. Это **не** публичный сертификат сайта и не сертификат Let's Encrypt. -- **Alerts** — уведомления как минимум о нехватке диска, исчерпании подключений, высокой загрузке CPU/IO, replication lag (если есть реплики), неуспешном backup и истечении/замене CA. -- **Deletion protection** — запрет удаления кластера обычной командой/API. Отключение защиты должно быть отдельным подтверждаемым действием администратора. Эта настройка не защищает от `DROP TABLE`, поэтому least privilege и backup всё равно обязательны. - -CA managed PostgreSQL скачать из панели или документации провайдера в защищённый путь VM, например `/opt/han-chat/secrets/pg/ca.pem`, с владельцем `root:deploy` и mode `0440` (либо `0400`, если файл читает один пользователь). Все DSN используют `sslmode=verify-full` и `sslrootcert=/run/secrets/pg-ca.pem` либо эквивалент драйвера. Режимы `disable`, `allow`, `prefer` и `require` без проверки CA для production запрещены. - -Публичные HTTPS-сертификаты `` и `` выпускаются отдельно через **Let's Encrypt** на Stage 9 и устанавливаются только в nginx соответствующей VM, а не в PostgreSQL. - -### 6.2. Роли - -Целевая модель разделяет: - -- admin/bootstrap role; -- migration role каждого schema с DDL; -- runtime role без DDL. - -Прототип `init-managed-postgres.py` выдаёт runtime roles `CREATE` на schema и печатает DSN. Это допустимо только для bootstrap/dev, но **слишком широко для production runtime**. Перед production адаптировать: - -1. создать шесть schemas: `han_app`, `bitrix_local`, `bitrix_sync`, `keycloak`, `message_safety`, `sms`; -2. создать runtime roles; -3. создать migration roles либо controlled admin job; -4. schema owner = migration role; -5. runtime: `USAGE`, DML и sequence grants только на свои objects; -6. `ALTER DEFAULT PRIVILEGES` от migration owner; -7. запретить чужие schemas и public schema create; -8. `bitrix_sync_user` получает только column/table grants и approved procedures из module-07 §13 после применения полной sync migration; broad schema write запрещён. - -Команда bootstrap требует ``: - -```bash -cd -cp deploy/pg-init.env.example deploy/pg-init.env -chmod 600 deploy/pg-init.env -# заполнить private host/database/admin и generated passwords -set -a; source deploy/pg-init.env; set +a -python3 deploy/init-managed-postgres.py -unset HAN_PG_ADMIN_PASSWORD -``` - -Не сохранять stdout с DSN в shared logs. Исторический `init-managed-postgres.sql` содержит placeholder passwords и database `postgres`; для целевой БД применять только после review и замены database name. - -### 6.3. Проверка least privilege - -Для каждого runtime user: - -```bash -psql "host= port= dbname= user= sslmode=verify-full sslrootcert=" \ - -c "select current_user, current_setting('search_path');" -``` - -Негативно проверить `CREATE TABLE` и доступ к чужой schema — они должны завершиться permission denied. - -### 6.4. Migration policy - -Порядок ownership: - -1. `api-backend` Alembic владеет `han_app`, triggers, seed; -2. `bitrix-local-app` Alembic владеет `bitrix_local`; -3. `message-safety` stub не создаёт PG tables до production implementation; -4. `bitrix-sync` migration role владеет schema `bitrix_sync`; api-backend Alembic отдельно мигрирует shared `han_app.sync_queue`, triggers и mapping; -5. Keycloak мигрирует standard tables сам; custom provider имеет собственные versioned migrations; -6. `sms-service` владеет versioned migrations/seed schema `sms`; runtime `sms_user` не имеет доступа к `han_app`/`keycloak`. - -Только expand/migrate/contract. Destructive migration — отдельный backup, approval и release. Downgrade data migrations не обещается; rollback приложения требует backward-compatible schema. - -### Gate 3 - -- [ ] Backups/PITR/TLS/deletion protection включены. -- [ ] Шесть schemas/roles созданы, включая `sms`/`sms_user`. -- [ ] Runtime roles не имеют DDL/чужого доступа. -- [ ] Migration credentials отделены от runtime. -- [ ] Empty/previous-version migration test успешен. -- [ ] PITR restore point создан перед первым release. - -**Ожидаемый результат:** runtime `SELECT 1` успешен, unauthorized schema read/create запрещены. - -## 7. Stage 4 — S3 buckets, IAM, CORS и lifecycle - -Создать три приватных bucket: - -- `-quarantine`; -- `-attachments`; -- `-documents`. - -Public ACL/listing выключены. Versioning включить для data buckets по policy; server-side encryption включить. - -IAM: - -- API role/key: exact prefixes, presign PUT quarantine, Head/copy/delete quarantine, write/read data; -- Safety role/key: **read-only quarantine**; -- backup/ops role: отдельно; -- frontend: никаких permanent credentials. - -CORS quarantine: - -```json -[ - { - "AllowedOrigins": ["https://"], - "AllowedMethods": ["PUT"], - "AllowedHeaders": ["Content-Type", "If-None-Match", "x-amz-checksum-sha256", "x-amz-*"], - "ExposeHeaders": ["ETag", "x-amz-checksum-sha256"], - "MaxAgeSeconds": 600 - } -] -``` - -Required signed headers для target flow: `Content-Type`, `If-None-Match: *`, checksum. Не разрешать `*` origin с credentials. - -Lifecycle: - -- quarantine: failed/orphan expire через 48 ч и только при отсутствии active `safety_tasks`; -- incomplete multipart upload: abort через 1 день; -- attachments/documents: без auto-delete до legal retention; -- noncurrent versions: policy после legal review. - -### Gate 4 - -- [ ] Все buckets private. -- [ ] API key не может list/write вне exact scope. -- [ ] Safety key не может write/delete. -- [ ] Browser test origin выполняет presigned PUT с checksum и `If-None-Match: *`; повтор того же key получает `412`. -- [ ] Complete фиксирует authoritative `version_id`, ETag и checksum; Safety читает только эту version. -- [ ] Wrong version/ETag и изменённый source дают deny/error и не promote-ятся. -- [ ] Conditional promote mismatch не создаёт delivery outbox/Bitrix call. -- [ ] Quarantine lifecycle не удалит active `safety_tasks`. -- [ ] Data lifecycle соответствует retention. - -**Ожидаемый результат:** anonymous GET/PUT получает deny; API capability test проходит. - -## 8. Stage 5 — repository и release layout - -На VM: - -```text -/opt/han-chat/ - backend/ # checkout текущего release - releases// # optional immutable release dirs - secrets/ # не в git - backups/ # только metadata/short-lived encrypted artifacts -``` - -Рекомендуемый rollout — immutable images из registry. Build на VM допустим для MVP, но требует reproducible Dockerfiles и достаточно диска. - -```bash -sudo install -d -m 0755 -o deploy -g deploy /opt/han-chat -git clone -cd -git fetch --tags -git checkout --detach -git status --short -``` - -Ожидается clean tree. Запретить deploy из mutable branch без recorded SHA. - -Проверить структуру: root `docker-compose.yml`, service directories, `nginx`, `keycloak`, `redis`, `observability`, frontend artifact. - -### Gate 5 - -- [ ] Checkout exact SHA/tag. -- [ ] Working tree clean. -- [ ] Images/Dockerfiles pinned, `latest` отсутствует. -- [ ] SBOM/vulnerability scan без unresolved critical/high. -- [ ] Root Compose — единственный production entrypoint. - -## 9. Stage 6 — несекретный `.env` и runtime secrets - -### 9.1. Создание - -```bash -cd -umask 077 -cp .env.example .env -chmod 600 .env -``` - -`.env` содержит только несекретный config и `SECRETS_SOURCE=file|selectel`. -Пароли, токены, access keys, credential-bearing DSN и `*_FILE` пути в нём -запрещены. Секреты выдаёт единый интерфейс: - -```bash -deployment/secrets/han-secrets run --config .env -- -``` - -Launcher устанавливает `HAN_SECRETS_ACTIVE=1`, не выводит значения и может -передать paths-only manifest через `HAN_RUNTIME_SECRET_MANIFEST`. Файлы manifest -должны быть абсолютными, недоступными group/other. Генерацию выполняет secret -manager; не выполнять `export SECRET=...` и не вставлять значения в history. - -### 9.2. Обязательные группы - -- `APP_ENV`, release/version, log level; -- private PG host/port/database и TLS CA в config; runtime DSN в secret backend; -- Redis ACL credentials/URLs DB0/1/2 только в secret backend; -- public web/API/auth URLs; -- Keycloak realm/audience/hostname/bootstrap/provider technical secrets; -- SMS DB URL, парные Keycloak↔SMS tokens, Direct `TOKEN_1`, callback URL и отдельные callback credentials; -- paired service tokens из arch-02; -- Bitrix client/application/webhook/encryption secrets; -- S3 endpoint/buckets/API and read-only Safety credentials; -- OTEL endpoint/remote exporter secrets; -- nginx/TLS/rate limits; -- frontend public build values. - -Пары должны совпасть: - -```text -BITRIX_LOCAL_APP_INTERNAL_TOKEN == BITRIX_INTERNAL_API_TOKEN -BITRIX_API_FORWARD_TOKEN == BITRIX_API_INBOX_TOKEN -KEYCLOAK_SMS_SERVICE_TOKEN == SMS_SERVICE_TOKEN -``` - -Service token и webhook token — разные secrets. - -### 9.3. Validation - -Запустить `scripts/validate-env` в двух независимых режимах: - -- config: mandatory not empty, `SECRETS_SOURCE`, запрет secret keys/DSN credentials; -- runtime: manifest/files или child environment без печати значений; -- нет `change-me`, example IP/domain, default OTP; -- URLs have correct schemes; -- public URLs HTTPS, internal URLs service DNS; -- PG TLS enabled; -- paired tokens equal; -- CORS/origins exact; -- no duplicate keys; -- `FRONTEND_DEV_PROXY_ENABLED=false`; -- Safety timeout согласован с nginx; -- secrets minimum length; -- mock OTP risk flag explicitly accepted. -- placeholders `change-me`/`<...>` запрещены; real mode требует sender/template/API key/callback credentials и recorded static egress IP; - -```bash -cd -./scripts/validate-env .env -sudo systemctl restart han-secrets@production.service -sudo ./scripts/validate-env .env \ - --runtime-manifest /run/han-chat/secrets/manifest -sudo deployment/secrets/han-compose config --quiet -``` - -`docker compose config` может раскрыть resolved secrets; не сохранять/публиковать его stdout. - -### Gate 6 - -- [ ] `.env` отсутствует в git и содержит только несекретный config. -- [ ] Secret launcher и permissions manifest/files проверены. -- [ ] Все placeholder/default secrets отклонены. -- [ ] Paired tokens совпадают. -- [ ] DSN private/TLS; URLs/issuer согласованы. -- [ ] Validation и Compose interpolation успешны. -- [ ] Secret recovery/rotation owner назначен. - -## 10. Stage 7 — images и frontend static - -### Pull-вариант - -```bash -cd -docker login -docker compose pull -docker image ls --digests -``` - -Registry token read-only и короткоживущий. - -### Build-вариант - -```bash -cd -DOCKER_BUILDKIT=1 docker compose build --pull -``` - -Build не получает production secrets. Записать image digests и release file -manifest (`path`, owner/group, mode, executable). Sync с blanket -`--chmod=F644` запрещён: scripts, preflight и hooks устанавливаются -`0755/0750` по manifest и проходят `test -x` до activation. - -Frontend: - -```bash -cd -npm ci -npm run test -npx expo export --platform web -``` - -Скопировать artifact в versioned `frontend-static` volume/image. `index.html` revalidate, hashed assets immutable. Build env содержит только public URL/realm/client id. Проверить отсутствие service tokens/mock OTP/S3 keys командой secret scanner. - -### Gate 7 - -- [ ] Все images доступны по digest. -- [ ] Frontend build/tests успешны. -- [ ] Static artifact не содержит secrets/source maps по policy. -- [ ] nginx image/config содержит request-id module и TLS features. -- [ ] Disk после pull/build >30% free. - -## 11. Stage 8 — root Compose, networks и volumes - -До запуска: - -```bash -cd -docker compose config --services -``` - -В target release: - -- root Compose ВМ1: edge `nginx`, `api-backend`, `keycloak`, `sms-service`, `sms-worker`, `bitrix-local-app`, Redis DB0/DB1, local `otel-collector`; -- root Compose ВМ2: собственный public/private nginx, `message-safety-api`, `message-safety-worker`, `clamd`, `freshclam`, `bitrix-sync`, Redis Safety, local `otel-collector`; -- API и worker Safety используют один immutable image, но отдельные processes/containers. MVP: 1 API + 1 worker container с 5 file-worker slots; при провале gates сначала увеличиваются slots/worker replicas по queue depth. - -Networks: - -- `public`: nginx и минимально Keycloak/frontend path; -- `backend`: internal services/Redis; -- `egress`: только утверждённые outbound workers; Keycloak в неё не входит, `sms-worker` входит; -- `observability`: services + Collector. - -Volumes: - -- `redis-data`; -- ACME certs/webroot; -- frontend static; -- `otel-queue`; -- никаких PG data volumes. - -Каждый `otel-queue` перед collector подготавливает idempotent -`otel-queue-init` (`10001:10001 0700`, без сети/secrets, только -`CHOWN/FOWNER`); collector стартует только после успешного one-shot. - -Проверить: - -```bash -docker compose config | rg 'ports:|expose:|networks:|volumes:' -``` - -Если `rg` на VM нет, использовать reviewed script, не ручной визуальный просмотр. Единственные published mappings — nginx 80/443. - -Redis: ACL, AOF everysec, RDB, maxmemory, volume, no host port. OTEL: config read-only, queue bounded, no public OTLP. - -### Gate 8 - -- [ ] Только nginx публикует ports. -- [ ] Internal services не подключены к public без причины. -- [ ] Named volumes созданы и permissions проверены. -- [ ] OTEL ownership init завершился `0`, а write-test проходит от collector - UID; root collector/`0777` не используются. -- [ ] Container resource limits/healthchecks заданы. -- [ ] Healthcheck-команды выполнены в exact pinned digests; отсутствуют - `ExitCode 127` и зависимости от несуществующих `wget`/`curl`. -- [ ] `docker compose config --quiet` success. - -## 12. Stage 9 — TLS bootstrap, фаза 1 - -Прототип `ssl-issue.sh` останавливает весь Compose и использует standalone Certbot. Для full stack предпочтителен **webroot two-phase**, чтобы не делать `compose down`. Процедура выполняется независимо в root Compose каждой VM: для ВМ1 с ``, для ВМ2 с ``. - -### Phase A: HTTP bootstrap - -1. DNS уже указывает на VM. -2. Запустить nginx с bootstrap config: только `/.well-known/acme-challenge/` и redirect; TLS block не требует отсутствующий cert. -3. Запустить Certbot profile: - -Сначала проверить процесс через Let's Encrypt staging CA, добавив `--staging`. Staging-сертификат не является доверенным браузерами и нужен только для проверки DNS, firewall, ACME webroot и конфигурации. После успешной проверки удалить staging lineage либо выпустить production-сертификат с отдельным `--cert-name` текущего host. Ни сертификат, ни ACME volume между VM не разделяются. - -Production-выпуск: - -```bash -cd -docker compose --profile tls-bootstrap up -d nginx -docker compose --profile certbot run --rm certbot certonly \ - --webroot -w /var/www/certbot \ - -d \ - --cert-name \ - --email \ - --agree-tos --no-eff-email --non-interactive -``` - -Каталоги `/etc/letsencrypt` и `/var/www/certbot` должны быть общими named volumes для контейнеров `certbot` и `nginx`. Сертификат и закрытый ключ не копируются в репозиторий или image. TLS-конфигурация nginx использует: - -```text -/etc/letsencrypt/live//fullchain.pem -/etc/letsencrypt/live//privkey.pem -``` - -### Phase B: TLS activation - -```bash -cd -docker compose exec -T nginx nginx -t -c /tmp/nginx.conf -# активировать rendered TLS config атомарно -docker compose kill -s HUP nginx -``` - -HSTS пока не включать. Проверить chain/hostname/redirect, затем включить HSTS без preload. - -### Автоматическое обновление Let's Encrypt - -Сертификаты Let's Encrypt действуют 90 дней. Проверку обновления выполнять **дважды в сутки**; Certbot сам обновляет сертификат только при приближении срока истечения. Для VM предпочтителен systemd timer, который вызывает репозиторный скрипт root Compose. - -Скрипт `/deploy/ssl-renew.sh` должен: - -1. взять `flock`, чтобы исключить параллельные запуски; -2. выполнить `docker compose --profile certbot run --rm certbot renew --webroot -w /var/www/certbot --quiet`; -3. при успешном обновлении проверить `docker compose exec -T nginx nginx -t -c /tmp/nginx.conf`; -4. только после успешной проверки выполнить `docker compose kill -s HUP nginx`; -5. записать структурированный результат и метрику времени до истечения; -6. вернуть ненулевой exit code при ошибке, чтобы сработал alert; -7. не удалять действующий сертификат при неуспешном renew. -8. на success path вернуть `0` с пустым stderr; benign output `nginx -t` и - signal command подавить/перенаправить, полную диагностику печатать только - при ошибке. - -Пример unit `/etc/systemd/system/han-chat-cert-renew.service`: - -```ini -[Unit] -Description=Renew HAN Chat Let's Encrypt certificate -Requires=docker.service -After=docker.service network-online.target - -[Service] -Type=oneshot -User=deploy -WorkingDirectory= -ExecStart=/deploy/ssl-renew.sh -``` - -Пример timer `/etc/systemd/system/han-chat-cert-renew.timer`: - -```ini -[Unit] -Description=Check HAN Chat certificate renewal twice daily - -[Timer] -OnCalendar=*-*-* 03,15:20:00 -RandomizedDelaySec=30m -Persistent=true -Unit=han-chat-cert-renew.service - -[Install] -WantedBy=timers.target -``` - -Установка и обязательная проверка расписания: - -```bash -sudo systemctl daemon-reload -sudo systemctl enable --now han-chat-cert-renew.timer -sudo systemctl list-timers han-chat-cert-renew.timer -cd -docker compose --profile certbot run --rm certbot renew --dry-run -sudo systemctl start han-chat-cert-renew.service -sudo systemctl status han-chat-cert-renew.service --no-pager -``` - -Дополнительно настроить alert при остатке менее 21 дня и критический alert менее 7 дней. Проверять срок можно synthetic probe снаружи и метрикой exporter/скрипта на VM. Ошибка одного запуска renew не должна останавливать nginx. - -Прототипные [`ssl-common.sh`](../../HAN_chat/bitrix-local-app/deploy/ssl-common.sh), [`ssl-renew.sh`](../../HAN_chat/bitrix-local-app/deploy/ssl-renew.sh) полезны концептуально, но должны работать с **root Compose**, не service compose. - -### Gate 9 - -- [ ] Staging issuance rehearsal успешен. -- [ ] Production cert chain/hostname valid. -- [ ] HTTP только ACME + 308. -- [ ] TLS 1.0/1.1 rejected; 1.2/1.3 accepted. -- [ ] Renewal dry-run и safe reload успешны. -- [ ] `han-chat-cert-renew.timer` включён, имеет следующий запуск и переживает reboot. -- [ ] Alert expiry настроен. - -## 13. Stage 10 — миграции и seed - -Остановить public traffic либо использовать maintenance page до gate. - -### 13.1. Preflight - -```bash -cd -docker compose run --rm api-backend alembic current -docker compose run --rm bitrix-local-app alembic current -``` - -Создать PITR marker. Выполнить dry-run/SQL review в clone/staging. - -### 13.2. Upgrade - -```bash -cd -docker compose run --rm api-backend alembic upgrade head -docker compose run --rm bitrix-local-app alembic upgrade head -``` - -Для message-safety stub PG migration отсутствует. Перед full sync cutover сначала применяется backward-compatible api-backend migration shared queue/mapping/trigger, затем migration schema `bitrix_sync`; grants выдаются после обеих migrations и negative permission test. Keycloak стандартную schema мигрирует выбранная pinned версия при controlled startup; provider migration выполняется отдельным approved job. - -### 13.3. Seed `app_settings` - -Seed обязан быть idempotent/versioned и содержать все ключи arch-04. Выполнить migration или: - -```bash -cd -docker compose run --rm api-backend python -m app.cli.seed_settings --file /deploy/app-settings.production-like.yaml -docker compose run --rm api-backend python -m app.cli.validate_settings -``` - -Команды являются целевым интерфейсом; если CLI ещё не реализован, gate не проходить ручными ad-hoc INSERT без reviewed SQL. - -Проверить public keys/DTO, consent versions/URLs, CORS host, file MIME/size, UX idle timeout. Не копировать phone/URLs из prototype без product approval. - -### Gate 10 - -- [ ] PITR marker до migrations. -- [ ] Expected Alembic revisions active. -- [ ] Runtime users не выполняли DDL. -- [ ] Seed idempotency проверена повторным запуском. -- [ ] Mandatory settings valid; secrets отсутствуют в `app_settings`. -- [ ] Backward compatibility с текущими images подтверждена. - -### 13.4. Controlled rollout real SMS - -До переключения Keycloak: - -1. применить App DB seed `otp.phone.code_length`, `otp.phone.ttl_seconds`, `otp.phone.sms_order_timeout_ms`; -2. создать schema/role `sms`, применить migrations и idempotent seed `sms_setting`/active approved `auth_otp`; -3. в test environment развернуть `sms-service`/worker с локальным mock Direct и выполнить contract/E2E; -4. получить production Direct `TOKEN_1`, согласованные sender и template, отдельные callback credentials; -5. определить egress IP фактическим запросом из `sms-worker`, подтвердить его статичность/NAT, записать в inventory и передать Direct для allowlist; -6. развернуть production `sms-service`/worker и callback route, оставив `KEYCLOAK_OTP_MOCK_ENABLED=true`; -7. применить Keycloak expand migration/SPI, мигрировать старые challenges по module-11; -8. выполнить provider smoke отдельной ops-командой на ``; проверить journal, callback, redaction и отсутствие duplicate; -9. только после подписанных evidence переключить `KEYCLOAK_OTP_MOCK_ENABLED=false`; -10. проверить durable order до Direct response, resend/superseded, expiry snapshot, limits и verify при provider reject/timeout. - -Production cutover запрещён при любом placeholder, несогласованном sender/template, отсутствующем API key/callback credentials, неподтверждённом callback IP или нестатическом egress IP. Direct API key — готовый `TOKEN_1` для Basic, повторно Base64 не кодируется. - -Rollback SMS: немедленно вернуть Keycloak в mock mode; не удалять schema/journal и не откатывать migrations без доказанной backward compatibility. Остановить новые real orders, дать worker завершить либо зафиксировать in-flight/`uncertain`; предпочтителен forward-fix. - -### 13.5. Controlled rollout `bitrix-sync` - -До включения `BITRIX_SYNC_ENABLED=true`: - -1. создать/проверить custom Contact fields `user_id`, registration flag, citizenship и занести их non-secret names в env; для universal CRM имя `UF_CRM_` преобразуется в `ufCrm_`, active filter использует `1`, а wire-значения add/update подтверждаются contract test; -2. создать smart process «Конфликты синхронизации», поля/стадии/ответственного/SLA и активировать validated `bitrix_sync.settings`; -3. завести отдельный входящий webhook технического пользователя с минимальными правами module-07 §13; -4. настроить два HTTP-webhook робота на Contact/alert receiver URLs `https:///bitrix/sync/webhook/...?token=`; использовать отдельные высокоэнтропийные query tokens из штатного secret manager, исключить query/body из журналов; local app/event handler для CRM sync не создавать; -5. применить expand migrations `han_app`, затем `bitrix_sync`, после чего выдать точечные GRANT и выполнить negative permission tests; -6. зафиксировать `cutover_watermark` и one-shot операцией отменить существующие до него pending/retry contact-задачи с причиной `initial_full_sync_cutover`; -7. не создавать backfill: это утверждённое ограничение первого релиза; -8. запустить image с sync disabled, проверить `/health/live`, expected `sync_disabled`, secret/config validation, portal host/member ID и smoke разрешённых Bitrix methods, включая `crm.item.list` для `entityTypeId=3` с `>=updatedTime`, `opened=1` и registration field `=1`; -9. открыть на nginx ВМ2 только два exact webhook routes для version-controlled `BITRIX_WEBHOOK_ALLOWED_CIDRS`, выполнить nginx config test, valid/invalid source IP и query-token form-urlencoded contract tests, подтвердить отсутствие query/body в logs/traces и отсутствие запросов на ВМ1; -10. включить sync, проверить обработку только post-watermark canary user, mapping в `bitrix_sync`, отсутствие CRM ID в App DB, suppression, alert/rebind и telemetry; -11. наблюдать не менее agreed canary window queue age, CRM limit errors, DLQ, webhook/reconciliation lag, число Contact, восстановленных reconciliation без webhook, source-IP rejects и SigNoz alerts; scheduled full reconciliation не запускать. - -Rollback: закрыть public webhook routes на nginx ВМ2 либо вернуть retryable `503`, остановить claims, bounded drain in-flight, установить `BITRIX_SYNC_ENABLED=false`. ВМ1 не изменяется. Уже созданные Contact/mapping автоматически не удалять; pending после watermark не отменять; schema downgrade с production rows не выполнять. - -#### Изменение source IP Битрикс24 - -Сигнал для проверки allow-list — всплеск Contact, восстановленных инкрементальной reconciliation без обработанного webhook, особенно одновременно с ростом `webhook_rejected_total{reason="source_ip"}`. - -1. Сопоставить окно всплеска с bounded-retention журналом source-IP rejects nginx ВМ2; query и body не извлекать и не сохранять. -2. Подтвердить принадлежность нового адреса инфраструктуре Битрикс24/портала по согласованному каналу или контролируемым probe. Наличие корректного query token само по себе не является подтверждением. -3. Добавить минимально необходимый IP/CIDR в version-controlled `BITRIX_WEBHOOK_ALLOWED_CIDRS`, выполнить peer review, manifest validation и `nginx -t` через штатный deployment unit. -4. Применить safe reload, проверить приём Contact/alert webhook и отсутствие query/body в logs/traces. -5. Убедиться, что source-IP rejects прекратились, webhook lag нормализовался, а следующие инкрементальные reconciliation run не показывают растущих восстановлений. -6. При ошибочном расширении немедленно вернуть предыдущую approved версию allow-list. Автоматическое добавление наблюдаемого IP запрещено. - -## 14. Stage 11 — Keycloak bootstrap - -### 14.1. Первый старт - -Запустить PostgreSQL-ready Keycloak отдельно: - -```bash -cd -docker compose up -d keycloak -docker compose ps keycloak -docker compose logs --since=10m keycloak -``` - -Bootstrap admin secret существует только на первый запуск. После создания named admin с MFA удалить/ротировать bootstrap credential из runtime env. - -### 14.2. Realm - -Clean environment может импортировать secret-free `han-chat` realm template. Живой production realm нельзя перетирать `--import-realm` без diff. - -Проверить: - -- client `han-chat-frontend`, public, PKCE S256; -- direct/implicit/password/social disabled; -- audience `han-chat-api`; -- exact redirect/web origins; -- issuer `https:///auth/realms/han-chat`; -- claims `sub`, `phone_number`, verified, audience; -- custom phone OTP provider; -- settings bridge token/path; -- mock code non-default и не виден UI/log; -- brute-force, token/session TTL, refresh rotation; -- admin console limited by VPN/allow-list. - -### 14.3. Provider migration - -Custom OTP tables мигрируются versioned mechanism до включения flow. Не редактировать standard Keycloak tables вручную. - -### Gate 11 - -- [ ] Discovery/JWKS public через HTTPS. -- [ ] Issuer exact, no internal hostname. -- [ ] Realm drift check clean. -- [ ] Только Authorization Code + PKCE S256. -- [ ] OTP wrong/replay/limit tests fail safely. -- [ ] Settings bridge cache/fail-closed tested. -- [ ] Bootstrap admin removed; named admin MFA enabled. - -## 15. Stage 12 — ordered startup и health gates - -Архитектурный порядок: - -1. На ВМ2 approved systemd unit поднимает Redis Safety и local Collector. -2. Затем `clamd`/`freshclam`, Safety API/worker и `bitrix-sync`. -3. Последним на ВМ2 поднимается nginx с независимыми public `80/443` и private `8443` server blocks; проверяются оба TLS-контура, exact webhook routes, capability health, signature age и отрицательные ingress/egress tests. -4. На ВМ1 unit поднимает локальные Redis/Collector, API, SMS, Keycloak, local app и edge nginx. -5. Только private `MESSAGE_SAFETY_URL` ВМ1 переключается на ВМ2 после Safety gates. Public CRM webhook DNS/routes ВМ2 разворачиваются независимо и не требуют изменения ВМ1. - -Legacy single-VM `docker compose up` из старого stub-контура не является evidence готовности target ВМ2. - -При повторной раскатке reload после readiness upstream обязателен: nginx -разрешает Docker DNS при загрузке конфигурации и иначе может продолжить -обращаться к старому container IP. Bare-команды `nginx -t` и -`nginx -s reload` не использовать: рабочий config/PID находятся в `/tmp`, а -filesystem контейнера read-only. - -После каждого шага ждать health, но проверять readiness отдельно из internal network: - -```bash -docker compose exec -T api-backend python -c "" -``` - -Не использовать host ports для curl. Допустим dedicated toolbox container в `backend` network. - -Expected: - -- Redis `PONG`; -- Keycloak DB/realm/provider ready; -- Collector health + exporter queue; -- Safety v2 capability ready и Redis Safety (target); legacy DB2 проверяется только в stub acceptance; -- API DB/Redis/JWKS/settings/S3/Safety ready; -- local app до Bitrix install может быть `portal_not_installed`; -- bitrix-sync до enablement возвращает `sync_disabled`; после preflight — `mode=full`, validated settings/secrets/grants, живые worker/limiter и актуальные reconciliation cursors; -- nginx config test success. - -### Gate 12 - -- [ ] Все containers live, нет restart loop/OOM. -- [ ] Critical readiness green. -- [ ] Expected degraded statuses только документированные; sync stub mode отсутствует. -- [ ] `docker compose ps` не публикует internal ports. -- [ ] Internal `/internal/*` снаружи 404. -- [ ] OTEL принимает telemetry. - -## 16. Stage 13 — Bitrix24 local app и Open Lines - -Bitrix admin создаёт local application: - -- install URL `https:///bitrix/install`; -- handler URL `https:///bitrix/handler`; -- placement URL `https:///bitrix/placement`; -- required scopes по module-06; -- client id/secret загружены в secret store до install; -- portal/domain allow-list exact. - -Выполнить install в Bitrix24. Local app должен сохранить encrypted OAuth, затем: - -1. `imconnector.register` connector `han_mobile_app`; -2. `imconnector.activate` line `8`; -3. `event.bind`; -4. status/reconciliation. - -Не использовать prototype `/bitrix-internal/internal/v1/*`: canonical path только internal Docker `/internal/openlines/v1/*`, наружу он отсутствует. - -Проверить status из toolbox/internal network с Bearer token, не печатая token: - -```bash -cd -docker compose run --rm --no-deps \ - -``` - -Создать test dialog/message через public API, не прямым legacy payload с телефоном. Проверить mapping и ответ оператора. - -### Gate 13 - -- [ ] OAuth stored encrypted; token не в logs. -- [ ] Connector configured/active on line 8. -- [ ] Events bound exactly once. -- [ ] Outbound text reaches Open Lines once. -- [ ] Operator reply reaches API, затем delivery ack. -- [ ] Duplicate callback не создаёт duplicate message. -- [ ] Internal status с internet недоступен. - -## 17. Stage 14 — public smoke и E2E - -### 17.1. Независимые public ingress - -```bash -curl -I http:/// -curl -fsS https:///api/v1/public/app-config -curl -fsS https:///api/v1/public/content -curl -fsS https:///auth/realms/han-chat/.well-known/openid-configuration -curl -i https:///internal/safety/v2/messages/check -openssl s_client -connect :443 -servername -curl -I http:/// -curl -i https:///internal/sync/v1/status -curl -i https:///bitrix/sync/webhook/contact -openssl s_client -connect :443 -servername -``` - -Expected ВМ1: 308; public 200 strict DTO; discovery 200; internal 404; valid cert. Expected ВМ2: HTTP redirect/ACME policy, internal 404, GET webhook 405/404, valid отдельный cert. Valid/invalid POST webhook проверяется отдельным form-urlencoded contract test с разрешённого и запрещённого source IP без помещения query token в shell history или логи. - -### 17.2. Auth/frontend - -- guest открывает content без write; -- protected write без JWT → 401; -- consent → mock OTP → PKCE tokens; -- bootstrap не передаёт phone body; -- session-start создаёт `ux_session_id`; -- silent refresh работает без OTP; -- logout очищает tokens; -- wrong/replayed OTP не выдаёт tokens. -- real mode: durable order возвращает `sms_message_id` до Direct response; callback обновляет только SMS journal; resend делает старый challenge `superseded`. - -### 17.3. Message Safety правила stub - -Обязательные E2E: - -- text, начинающийся после normalization с `ф/Ф` → public `422 message_blocked`, Bitrix не вызван; -- legacy stub-only: text с цифры → v1 `203`/test terminal `400`; target v2 smoke использует `202`/`200|403|terminal 503`, клиент internal pending не получает; -- прочий text → allow; -- terminal stub `400` преобразуется в `422`, не в generic validation; -- timeout → `503/504`, checkpoint/recovery, без duplicate; -- один slow poll не блокирует другие requests. - -Статус `message-safety` должен быть явно `stub`; для real production он не заменяет antivirus/file scan. - -### 17.4. Files - -- allow image/PDF ≤ configured size; -- wrong extension+MIME/oversize/checksum reject; -- direct presigned PUT quarantine; -- allow promote attachments; -- deny остаётся вне data и quarantine cleanup; -- Safety read-only credential не может write; -- download URL owner-only + audit; -- presigned URL отсутствует в logs. - -### 17.5. Realtime и ownership - -- WS connects/subscribes; -- operator reply arrives; -- reconnect + REST gap reconciliation; -- polling fallback; -- чужие dialog/message/attachment/document id → 404; -- idempotency same body replay, changed body 409; -- rate limits 429 + `Retry-After`. - -### Gate 14 - -- [ ] Полный first-send flow успешен. -- [ ] Safety allow/deny/pending/timeout проверены. -- [ ] Text/file/WS/polling работают. -- [ ] Ownership и no-public-internal tests зелёные. -- [ ] Нет secret/PII/message body/presigned URL в logs. -- [ ] Audit events созданы. -- [ ] Bitrix получает только allowed message. - -## 18. Stage 15 — observability validation - -Следовать [`module-09-observability.md`](module-09-observability.md): - -1. послать request с известным `X-Request-ID`; -2. найти nginx log, API trace и downstream spans; -3. проверить `service.name`, environment, trace/request/UX ids; -4. при выбранном telemetry backend проверить dashboards всех services; до выбора — проверить bounded stdout/debug acceptance и явно зафиксировать ограничение; -5. trigger safe synthetic 4xx/5xx и проверить alert route, если backend с alerting уже выбран; -6. при настроенном remote OTLP временно блокировать его, проверить bounded queue и business continuity; -7. проверить Collector health/drop/refused; -8. выполнить PII/secret canary test. - -### Gate 15 - -- [ ] Три сигнала доступны. -- [ ] Trace cross-service связан. -- [ ] Для выбранного backend alerts доставляются on-call и SLO queries возвращают данные; иначе limitation/TBD явно принят и traffic не называется production-ready. -- [ ] Collector outage не ломает business path. -- [ ] Redaction test пройден. - -## 19. Stage 16 — opening traffic - -До открытия: - -- удалить maintenance response; -- включить HSTS после финального TLS test; -- сохранить release SHA/image digests/schema revisions/realm desired version; -- создать restore point; -- подтвердить on-call; -- не удалять previous images; -- observation window 60 минут. - -В первые 60 минут: 5xx, auth, message delivery, DB/Redis, memory, restart, Collector queue, Bitrix OAuth/DLQ. - -### Gate 16 - -- [ ] Все Gate 0–15 подписаны. -- [ ] Rollback release доступен. -- [ ] Backup/restore evidence свежий. -- [ ] Нет active page alert. -- [ ] Product owner принял stub limitations. - -## 20. Backup и restore - -### PostgreSQL - -- provider daily + PITR; -- перед migrations/Keycloak upgrade — manual restore point; -- ежеквартальный restore clone; -- проверить все schemas, Alembic/Keycloak revisions, keys, grants. - -### S3 - -- versioning/lifecycle data buckets; -- inventory/checksum при поддержке; -- restore не делает objects public; -- quarantine не является backup. - -### Redis - -AOF/RDB ускоряют restart, но не business backup. При corruption поднять clean Redis; API восстанавливает durable state из PG. Никогда не считать Redis dump достаточным для messages/audit. - -### Keycloak - -DB backup включает realm/users/signing keys/provider data. Secret-free realm export — config backup, не полный data backup. После restore проверить issuer/JWKS/PKCE/OTP/refresh. - -### Restore rehearsal - -1. isolated VPC/hostnames; -2. restore PG clone и S3 copies; -3. deploy same image digests; -4. не направлять production DNS/Bitrix callbacks; -5. run migrations only if required release; -6. smoke auth/chat without real operator impact; -7. record measured RPO/RTO; -8. destroy isolated secrets/resources controlled. - -## 21. Rollback - -### Application-only - -1. объявить incident/maintenance; -2. при SMS incident вернуть `KEYCLOAK_OTP_MOCK_ENABLED=true`, прекратить новые real orders и сохранить journal/in-flight state; -3. сохранить diagnostics и current state; -4. остановить новые claims/send при возможности; -5. переключить image tags на previous digests; -6. не выполнять Alembic downgrade; -7. `SCHEMA_BACKWARD_COMPATIBLE_CONFIRMED=true deployment/scripts/rollback.sh `; -8. health/smoke/idempotency; -9. проверить outbox/inbox/SMS pending/uncertain/recovery. - -Используется текущий runtime secret set и текущий несекретный config. Snapshot -старого `.env` не создаётся и не применяется. - -### После backward-incompatible migration - -Обычный rollback запрещён. Выбор: - -- forward-fix; -- restore PG PITR + coordinated S3/Bitrix reconciliation; -- maintenance до решения. - -Нельзя rollback DB отдельно от Keycloak signing/session state без анализа. - -### TLS/nginx rollback - -`nginx -t`; оставить старый config/workers при failure. Не удалять working cert. При ошибке renewal current cert остаётся, но alert. - -### Gate rollback - -- [ ] Previous images available. -- [ ] Schema совместима. -- [ ] No duplicate outbound after restart. -- [ ] Data reconciliation completed. -- [ ] Incident timeline содержит release/request ids без PII. - -## 22. Routine operations - -Ежедневно автоматически: - -- health/synthetics/SLO/alerts; -- PG backup/PITR status; -- TLS expiry/renew; -- disk/inodes/OTEL queue; -- Redis AOF/memory/evictions; -- DLQ/backlog/quarantine age; -- Keycloak signing/OAuth/settings cache. - -Еженедельно: - -- vulnerability/image updates review; -- failed login/rate-limit trend; -- Bitrix connector desired/observed; -- S3 lifecycle/inventory; -- restore/rollback artifacts availability. - -Ежемесячно: - -- patch OS/images in maintenance; -- secret/access review; -- capacity/cardinality/cost; -- stale users/admins; -- runbook sample drill. - -Команды: - -```bash -cd -docker compose ps -docker compose logs --since=15m -docker stats --no-stream -docker system df -docker compose exec -T nginx nginx -t -``` - -Не использовать unbounded `logs`, `docker system prune -a`, Redis `KEYS/FLUSH*` или ad-hoc DB DELETE. - -## 23. Incident commands - -Безопасный triage: - -```bash -cd -date -Is -docker compose ps -docker stats --no-stream -docker compose logs --since=10m --tail=500 -df -h -free -h -sudo ss -lntp -sudo iptables -L HAN-CHAT-DOCKER -n -v -``` - -PG: - -```bash -psql "" -c "select now(), count(*) from pg_stat_activity;" -``` - -Redis — только ops ACL: - -```bash -docker compose exec -T redis redis-cli --user --pass '' PING -``` - -Никогда не вставлять secret literal в ticket/chat. Предпочесть stdin/secret file. Не выполнять ручной replay message/DLQ до проверки idempotency и ambiguous Bitrix outcome. - -Типовые сценарии: - -- API 503: DB/Redis/JWKS/settings/Safety readiness и circuits; -- send timeout: safety checkpoint/outbox, не повторять с новым key; -- Bitrix down: OAuth/circuit/backlog/DLQ, reads оставить; -- Redis loss: clean restart, durable fallback, polling; -- PG outage: не restart storm; provider incident; -- disk full: остановить ingest growth, очистить только known cache/old image после inventory; -- cert near expiry: webroot/DNS/rate limit, staging rehearsal; -- secret leak: revoke/rotate, telemetry deletion, redeploy. - -## 24. Upgrades - -Общий порядок: - -1. release notes/security advisories; -2. compatibility matrix; -3. backup/PITR; -4. staging clone; -5. image/build/test/scan; -6. expand migration; -7. one service at a time по dependency order; -8. health/E2E/observation; -9. contract migration later; -10. record digests/revisions. - -Keycloak: не пропускать unsupported majors; проверить SPI/provider migration и JWKS. Redis: AOF compatibility/rewrite. Collector: config validate against exact version. nginx: `nginx -t` и TLS scan. PostgreSQL major upgrade сначала rehearsal clone. - -## 25. Disaster recovery - -### Потеря VM - -1. provision new Ubuntu VM в VPC; -2. применить reviewed hardening; -3. attach public IP/update DNS с low TTL; -4. restore secrets из vault, не со старого disk без проверки; -5. pull exact images; -6. mount/create volumes; Redis можно clean; -7. connect existing/restored PG/S3; -8. TLS issue/restore safely; -9. ordered startup/gates; -10. Bitrix callback/connectors verify; -11. public smoke, then traffic. - -### Потеря PG - -Restore PITR в new managed instance, private SG/TLS, update DSN, validate schemas/grants/revisions. Остановить writes до chosen restore point/reconciliation. S3 objects после restore point могут стать orphan; выполнить audit-backed reconcile. - -### Потеря S3 - -Без data backup/versioning полное восстановление невозможно. Временно отключить file operations, оставить text flow, restore objects/inventory, reconcile DB metadata, не генерировать URLs отсутствующих objects. - -### Compromise - -Isolate VM, preserve forensic snapshot, rotate all service/DB/S3/Bitrix/Keycloak secrets, revoke sessions/signing keys по масштабу, deploy clean VM/images, restore trusted data, notify по incident/legal process. - -## 26. Teardown cautions - -`docker compose down` не удаляет managed PG/S3, но может остановить callbacks. `down -v` удалит Redis/ACME/OTEL queue volumes и запрещён без approval. Запрещены: - -```text -docker compose down -v -docker system prune -a --volumes -DROP DATABASE / DROP SCHEMA -S3 recursive delete -cloud project/VPC delete -certbot delete active cert -``` - -Перед teardown: - -- export inventory/digests/config without secrets; -- revoke Bitrix app/callbacks; -- revoke/rotate credentials; -- backup/retention/legal hold; -- DNS drain; -- deletion protection removal — отдельное approval; -- проверить shared VPC/PG/S3; -- зафиксировать evidence уничтожения. - -## 27. Definition of Done - -- VM/VPC/DNS/SG/hardening соответствуют Gate 1–2; -- managed PG private/TLS/backups/least privilege/migrations работают; -- S3 private/IAM/CORS/lifecycle проверены; -- exact release/images/frontend deployed; -- non-secret `.env` validated; runtime secrets разделены по сервисам и защищены; -- `deploy` не имеет Docker/root-equivalent доступа; production files root-owned, sudo ограничен конкретными systemd-units; -- один root Compose и nginx на VM; ВМ1 и ВМ2 имеют независимые public 80/443, ВМ2 дополнительно private 8443; public ВМ2 ограничен exact CRM webhook; -- после Safety cutover ВМ1 не содержит local Safety/Redis DB2 и вызывает ВМ2 - только по private HTTPS с проверенным CA bind; -- container hardening и сетевые границы соответствуют arch-06; -- для каждой private/no-egress VM завершён и задокументирован lockdown с негативной проверкой внешнего доступа/egress; -- Redis/Collector volumes/resources/security работают; -- Keycloak realm/provider/PKCE/OTP готов; -- ordered startup/readiness пройден; -- Bitrix connector line 8 и callback flow проверены; -- full auth/text/file/realtime/safety E2E зелёный; -- observability/redaction проверены; SLO/alerts проверены для выбранного backend либо явно остаются принятым production-blocking TBD; -- backup restore и rollback rehearsed; -- ops/incident/upgrade/DR owners назначены; -- все assumptions/TBD приняты до открытия traffic. - -### VM2 cutover, rollback, reprovision и Freshclam - -Cutover gates: private TLS chain/SAN; Safety v2 PG migration и lease/fencing smoke; capability `text|links|files|worker`; S3 Gate 4; performance acceptance; egress negative tests. Safety Service Owner, Rule Pack Owner, Security Owner, Product Owner и Operations Owner фиксируют approvals. Только после них ВМ1 переключает `MESSAGE_SAFETY_URL`. Legacy v1 остаётся rollback target на ограниченное окно, но один `message_id` нельзя одновременно отправлять в v1 и v2. - -#### Обязательный gate миграции legacy ВМ1 - -Перед переключением caller на ВМ2 существующий backend ВМ1 считается legacy и -должен пройти отдельный migration gate: - -1. env validator принимает только production - `MESSAGE_SAFETY_URL=https://:8443`, требует - `MESSAGE_SAFETY_CA_HOST_PATH` и проверяет hostname/SAN; cross-host Docker - hostname и plaintext запрещены; -2. internal CA расположен в едином root-owned staging path, а не под - `deploy:deploy 0700`; positive read-test проходит от UID/GID - `api-backend`, negative — от постороннего UID; -3. local `message-safety`, его Redis DB2 и локальные rules-version env удалены - из target Compose/validator. Одновременный local и remote Safety запрещён; -4. release tree/Compose/unit принадлежат root, `deploy` исключён из `docker`, - установлен реальный root-owned stack systemd unit и permission preflight; -5. VM1 `DOCKER-USER` сопоставляет original published ports через - `conntrack --ctorigdstport`; counters подтверждены positive/negative probe - после Docker restart и reboot; -6. images закреплены digest, release file modes подтверждены manifest, - rollback выбирает совместимые digests/config, а не только строковый - `RELEASE_VERSION`; -7. ordered startup из этого runbook заменяет legacy `docker compose up -d` - всего стека; после readiness выполняется production nginx config - test/reload; -8. `han-secrets` и firewall oneshot после обновления явно перезапущены и - проверены по timestamp/live state; TLS renewal success path имеет пустой - stderr. - -Ни старый deployment guide, ни успешный запуск legacy single-VM Compose не -являются доказательством прохождения этого gate. - -Rollback возвращает caller adapter/upstream ВМ1 на предыдущий immutable release. Уже созданные v2 tasks завершаются/reconcile по PG checkpoints; down-migration и удаление quarantine versions запрещены. - -При потере ВМ2 fail-open запрещён. ВМ2 reprovision-ится из immutable image/config; secrets materialize под отдельным IAM, Redis поднимается пустым, migrations/capability/egress gates повторяются. RTO ≤4 ч; restore rehearsal минимум дважды в год. - -#### Message Safety config activation - -Первая migration создаёт `message_safety.config_versions` и seed version 1; readiness не открывается без ровно одной valid active version. Config-only rollout выполняется отдельным root-owned migration/config job под config-admin DB role: создать immutable draft, проверить JSON Schema/cross-field constraints и наличие rules/detector artifacts, записать approvals, затем транзакционно activate. Runtime API/worker имеют только `SELECT` к config table. - -После activation operator проверяет health `config_version`, text/link/file canaries, cache-key version и отсутствие изменения in-flight task version. Изменение, ослабляющее policy или увеличивающее limits, требует Security Owner; остальные — Safety Service Owner и Operations Owner. Rollback не реактивирует retired row: предыдущий payload клонируется в новую monotonic version и активируется с отдельным audit event. - -#### Emergency MOCK - -`deploy` может без root login включить/изменить/выключить mode: - -```bash -sudo /usr/local/sbin/han-message-safety-mode mock --text-free true --file-free true -sudo /usr/local/sbin/han-message-safety-mode mock --text-free true --file-free false -sudo /usr/local/sbin/han-message-safety-mode standard -``` - -Root устанавливает helper и `/etc/sudoers.d/deploy-message-safety-mode` при bootstrap. Sudoers разрешает `deploy` только этот immutable `root:root 0755` executable; helper имеет строгий parser без shell eval/path arguments, атомарно обновляет `root:han-message-safety 0640` `/etc/han-chat/message-safety-mode.env` (dedicated GID `10001` доступен только non-root контейнеру), валидирует Compose config, выполняет фиксированную Message Safety API recreate/restart operation внутри root project и проверяет private health. На ошибке он восстанавливает предыдущий mode/config и повторяет restart. `deploy` не получает write к config, systemd units, Compose и Docker socket. - -Перед включением operator фиксирует incident/change ID и выбранные text/file policies; после команды проверяет `processing_mode=mock`, forced canaries, отсутствие `202`, metric/active alert и audit actor. Ранее принятые standard tasks сохраняют mode и завершаются без переклассификации; только новые requests используют MOCK. Автоматического timeout нет: mode действует без ограничения по времени до явного `standard`. Поэтому перед закрытием incident обязателен возврат в standard, проверка normal capabilities и text/link/EICAR canary. File, разрешённый в MOCK, маркируется `scan_status=bypassed`, а не `clean`. - -`freshclam` имеет controlled egress только к утверждённому signature CDN. Seed `max_signature_age_hours` равен `240` ч (10 дней), а schema запрещает значения выше `720` ч (30 дней); stale/failed update выключает только `files` и поднимает alert. Новая база проходит integrity/load/EICAR canary и atomic activate/reload; при regression возвращается последняя валидная база. - -Initial ВМ2: 4 vCPU/8 GiB/80 GiB, resource/PID limits и backpressure. Workers масштабируются первыми по queue depth, `clamd` — scan lanes. ВМ3/scale-out инициируются при sustained CPU/RAM >70%, queue age >30 с, провале module-05 performance gates, contention `bitrix-sync` или независимом release cadence. - -## 28. Допущения, TBD и архитектурные конфликты - -### Допущения - -- D-A1: отдельные public hosts ВМ1/ВМ2; processing host публикует только CRM webhook, остальные сервисы ВМ2 остаются private. -- D-A2: обязательный минимум — `otel-collector`; доступность remote backend не предполагается до закрытия D-TBD11, local Grafana stack не обязателен. -- D-A3: managed provider даёт private network, TLS, backups/PITR. -- D-A4: Bitrix portal/connector/line остаются разрешёнными значениями architecture. -- D-A5: mock OTP временно разрешён до controlled SMS cutover как documented risk. - -### TBD до production - -- D-TBD1: реальные domains, Expo native redirect URI и Bitrix placement frame ancestors. -- D-TBD2: final VM1/PG sizing, public API/WS SLO и общие RPO/RTO; Safety load/latency gates уже зафиксированы, monthly availability SLO Safety в MVP намеренно не вводится. -- D-TBD3: legal retention/erasure для PG/S3/audit/telemetry. -- D-TBD4: secret manager и rotation windows. -- D-TBD5: реализовать/cutover production Safety v2; inbound operator files сознательно не проходят AV в MVP. -- D-TBD6: реализовать код, migrations, portal fields/smart process/webhooks и выполнить bitrix-sync cutover по module-07. -- D-TBD7: pinned Keycloak/nginx/Collector versions и SPI compatibility. -- D-TBD8: exact migration/seed/toolbox CLI commands после реализации repo. -- D-TBD9: Keycloak admin VPN/MFA topology. -- D-TBD10: final CSP/CORS/S3 headers и cloud-specific IAM. -- D-TBD11: уточнить auth/retention/alert route выбранного private SigNoz; backend и endpoint уже зафиксированы. - -### Обнаруженные конфликты - -1. `module-07` теперь задаёт полный target `bitrix-sync`; до реализации кода/migrations/portal prerequisites сервис обязан оставаться disabled, документация сама по себе не означает выполненный cutover. -2. Текущая implementation остаётся v1 stub; module-05 описывает draft target v2. Cutover является production gate. -3. Прототипный PG init даёт runtime role `CREATE` schema и не разделяет migration/runtime roles; runbook требует ужесточения. -4. Прототипные TLS scripts используют отдельный service Compose/standalone downtime, тогда как целевая архитектура требует root Compose и two-phase webroot. -5. Prototype публиковал `/bitrix-internal/*` и использовал `/internal/v1/*`; целевой контур это запрещает и использует `/internal/openlines/v1/*`. -6. Runtime artifacts `.env.example`/Compose ещё могут не содержать зафиксированные VM2 env; документация не означает выполненный cutover. -7. Точные RPO/RTO, SLO, Keycloak version и Bitrix retry semantics не утверждены; OTP TTL задаётся `app_settings`, SMS journal по module-11 хранится бессрочно. -8. `init-managed-postgres.py` по умолчанию не задаёт TLS parameters при bootstrap connection и печатает credential-bearing DSN; его production-hardening обязателен. -9. Текущие Compose/env/config artifacts могут ещё не содержать `sms-service`; документация не разрешает real mode до реализации и прохождения rollout gates. - -## 29. Ссылки на прототип - -- Исторические шаги: [`../../HAN_chat/Deploy_steps.md`](../../HAN_chat/Deploy_steps.md). -- Исторические команды эксплуатации: [`../../HAN_chat/backend-managing.md`](../../HAN_chat/backend-managing.md). -- VM baseline: [`../../HAN_chat/deploy/setup-vm-han-chat.sh`](../../HAN_chat/deploy/setup-vm-han-chat.sh). -- PG bootstrap: [`../../HAN_chat/deploy/init-managed-postgres.py`](../../HAN_chat/deploy/init-managed-postgres.py), [`../../HAN_chat/deploy/init-managed-postgres.sql`](../../HAN_chat/deploy/init-managed-postgres.sql), [`../../HAN_chat/deploy/pg-init.env.example`](../../HAN_chat/deploy/pg-init.env.example). -- Legacy nginx: [`../../HAN_chat/bitrix-local-app/deploy/nginx-tohin.ru.site.conf`](../../HAN_chat/bitrix-local-app/deploy/nginx-tohin.ru.site.conf), [`../../HAN_chat/bitrix-local-app/deploy/nginx/nginx.conf`](../../HAN_chat/bitrix-local-app/deploy/nginx/nginx.conf). -- Legacy TLS: [`../../HAN_chat/bitrix-local-app/deploy/ssl-issue.sh`](../../HAN_chat/bitrix-local-app/deploy/ssl-issue.sh), [`../../HAN_chat/bitrix-local-app/deploy/ssl-renew.sh`](../../HAN_chat/bitrix-local-app/deploy/ssl-renew.sh), [`../../HAN_chat/bitrix-local-app/deploy/ssl-install-cron.sh`](../../HAN_chat/bitrix-local-app/deploy/ssl-install-cron.sh). diff --git a/backlog.md b/support&knowlages/backlog.md similarity index 99% rename from backlog.md rename to support&knowlages/backlog.md index b07d0ef..da84743 100644 --- a/backlog.md +++ b/support&knowlages/backlog.md @@ -41,7 +41,7 @@ 7. #BACK_BUSINESS Описание бизнес сущностей: Сообщение 8. #UI Сделать страницу с инструкцией по установке приложения 9. #LEGAL Написать пользовательское соглашение. -10. #BACK_BUSINESS Разработка Message Safety v2 по [`module-05`](modules/module-05-message-safety.md), §18 DoR/DoD и cutover gates [`module-10`](modules/module-10-deployment-runbook.md): +10. #BACK_BUSINESS Разработка Message Safety v2 по [`module-05`](modules/module-05-message-safety.md), §18 DoR/DoD и cutover gates [`module-10-vm2`](modules/module-10-deployment-vm2.md): - API/OpenAPI v2, versioned `message_safety.config_versions`, configuration activation/validation и schema migrations; - PostgreSQL queue/lease/fencing/deadline + Redis hot cache/rate/wakeup; - Unicode normalization и versioned text rule bundle/corpus; diff --git a/faq.md b/support&knowlages/faq.md similarity index 100% rename from faq.md rename to support&knowlages/faq.md diff --git a/releases/#0 deploy-steps.md b/support&knowlages/releases/#0 deploy-steps.md similarity index 100% rename from releases/#0 deploy-steps.md rename to support&knowlages/releases/#0 deploy-steps.md diff --git a/releases/#1 SMS OTP deploy.md b/support&knowlages/releases/#1 SMS OTP deploy.md similarity index 100% rename from releases/#1 SMS OTP deploy.md rename to support&knowlages/releases/#1 SMS OTP deploy.md diff --git a/releases/#1.1 VM-service-deploy.md b/support&knowlages/releases/#1.1 VM-service-deploy.md similarity index 100% rename from releases/#1.1 VM-service-deploy.md rename to support&knowlages/releases/#1.1 VM-service-deploy.md diff --git a/releases/#2 notifications deploy.md b/support&knowlages/releases/#2 notifications deploy.md similarity index 100% rename from releases/#2 notifications deploy.md rename to support&knowlages/releases/#2 notifications deploy.md diff --git a/ops-monitoring/diagnose-han-chat.sh b/support&knowlages/scripts/diagnose-han-chat.sh similarity index 100% rename from ops-monitoring/diagnose-han-chat.sh rename to support&knowlages/scripts/diagnose-han-chat.sh diff --git a/ops-monitoring/han-vm-metrics.sh b/support&knowlages/scripts/han-vm-metrics.sh similarity index 100% rename from ops-monitoring/han-vm-metrics.sh rename to support&knowlages/scripts/han-vm-metrics.sh diff --git a/ops-monitoring/instructions.md b/support&knowlages/scripts/instructions.md similarity index 100% rename from ops-monitoring/instructions.md rename to support&knowlages/scripts/instructions.md diff --git a/ops-monitoring/message_lost_diagnostic.md b/support&knowlages/scripts/message_lost_diagnostic.md similarity index 100% rename from ops-monitoring/message_lost_diagnostic.md rename to support&knowlages/scripts/message_lost_diagnostic.md diff --git a/ops-monitoring/pg_connection_test.sh b/support&knowlages/scripts/pg_connection_test.sh similarity index 100% rename from ops-monitoring/pg_connection_test.sh rename to support&knowlages/scripts/pg_connection_test.sh diff --git a/ops-monitoring/send_sms.md b/support&knowlages/scripts/send_sms.md similarity index 100% rename from ops-monitoring/send_sms.md rename to support&knowlages/scripts/send_sms.md diff --git a/ops-monitoring/Обновление clamav образа.md b/support&knowlages/usefull ops/Обновление clamav образа.md similarity index 100% rename from ops-monitoring/Обновление clamav образа.md rename to support&knowlages/usefull ops/Обновление clamav образа.md