Compare commits

..

85 Commits

Author SHA1 Message Date
ivan
7b6a285741 uralkal: pm — один блок deployment в патчах backend и celery
Второй ключ deployment в патче отбрасывался, и args из оверлея не применялись:
/vault/secrets/pm-kafka инжектился, но не source-ился, pm ходил в Kafka с
дефолтными user/password. makemigrations из оверлея не включён.
2026-11-02 15:31:18 +05:00
ivan
7cacd5a03f uralkal: kafka-scram-users — automountServiceAccountToken для Vault injector 2026-11-02 15:19:13 +05:00
ivan
654803c2af uralkal: Job заводит SCRAM-пользователей Kafka из Vault kafka/apps/{pm,issues,flows}
Логин и пароль берутся из тех же путей Vault, что читают pm/message-hub и
issues/flows; пароль администратора — inter_broker_user из kafka/bootstrap
(роль Vault kafka, SA kafka-kafka-contour). После создания проверяется вход
каждого пользователя по SASL_SSL на 9092, пароли в лог не пишутся.
2026-11-02 15:11:03 +05:00
ivan
b9022560f3 ++ 2026-11-02 14:32:11 +05:00
ivan
10c04ba32b uralkal: pg-tls — пробы pg_isready с -U notes (п.31, INFRA-3062)
Без -U libpq ищет имя для UID 1001, которого нет в passwd вне nss-wrapper,
pg_isready падает и liveness убивает под через 90 с.
2026-11-02 14:05:14 +05:00
ivan
32e4bc657a uralkal: pg-tls — TLS и pg_hba аргументами postgres, writable root (п.31, INFRA-3062)
Образ contour/postgresql — официальный postgres с обёрткой contour, bitnami-переменные
TLS и extended-конфиг он игнорирует: ssl, сертификат, ssl_min_protocol_version и
hba_file передаются через -c; readOnlyRootFilesystem: false и пробы pg_isready как у
infrastructure/postgresql/uralkal.
2026-11-02 13:34:09 +05:00
ivan
5e5148e951 uralkal: тестовый PostgreSQL с TLS и notes-backend с verify-full (п.31, INFRA-3062)
ns postgres-tls-test: postgresql-contour pg-tls с tls.autoGenerated,
ssl_min_protocol_version TLSv1.2, pg_hba только hostssl (local для
администрирования); notes-tls на образе notes-backend с
PG_SSL_MODE/PGSSLMODE=verify-full и CA из pg-tls-crt.
regcred в ns создаётся вручную.
2026-11-02 13:14:59 +05:00
ivan
eb112cd4dd uralkal: образы eav и iam по digest (п.38, INFRA-3058) 2026-11-02 12:50:18 +05:00
ivan
56a75bf6a6 uralkal: образы remarks, reviews, prescriptions, projects, cross-section по digest (п.38, INFRA-3058)
Пилот на пяти статических фронтендах: image.name в оверлее uralkal
в виде <тег>@sha256:<digest>, digest манифестов из cr.yandex.
2026-11-02 12:46:35 +05:00
ivan
b7e7fc1c72 uralkal: drift detection для всех HelmRelease (п.39, INFRA-3058)
driftDetection.mode: enabled патчем в clusters/uralkal/kustomization.yaml,
base и другие контуры не затронуты. Исключены webhooks в
Mutating/ValidatingWebhookConfiguration (caBundle пишут vault-injector и
istiod) и /spec/replicas Deployment у istiod (HPA istio-system/istiod).
Kustomization flux-system уже prune: true, interval: 10m.
2026-11-02 12:32:59 +05:00
emelinda
30f1d32fc5 Add Vault audit logging integration for uralkal namespace with sidecars for file rotation, OTLP export, and Kafka integration 2026-11-02 09:22:20 +03:00
emelinda
2aaa45bf3c Add Vault audit logging integration for uralkal namespace with sidecars for file rotation, OTLP export, and Kafka integration 2026-11-02 02:24:00 +03:00
emelinda
2f5e37f598 **Add Zitadel events ingestion pipeline with AuthorizationPolicy and initial Job configuration for security-audit namespace** 2026-11-02 00:57:05 +03:00
emelinda
ae1a683ada **Add HelmRelease access-collector for processing ingress-gateway access logs in security-audit namespace with Kafka and Vault integration** 2026-11-02 00:33:20 +03:00
emelinda
ad7050ca7d Add audit collector HelmRelease for security-audit namespace to process and export kube-apiserver audit logs, including Kafka and Vault integration 2026-11-01 22:02:54 +03:00
emelinda
5525cc33ff Update CLAUDE.md to reflect revised cluster configurations, updated overlay conventions, and slice generation details. Clarify application, infrastructure, and routing specifics, including new clusters and template variables. 2026-11-01 21:35:44 +03:00
e4e5e06aec ++ camunda legacy wait 2026-10-31 17:45:29 +03:00
9f2c6932d7 ++ camunda timeout 30m 2026-10-31 15:21:43 +03:00
ivan
cd94931c76 ++ merge feat/zitadel-setup: sarex-installer machine user
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 13:53:12 +05:00
ivan
104bde49c4 ++ 2026-10-29 18:23:28 +05:00
ivan
b61571a858 ++ 2026-10-29 17:36:35 +05:00
13b9ca8a9f ++ stand chart repo from vars 2026-10-29 15:05:47 +03:00
ivan
31bdc74c80 ++ 2026-10-29 16:11:54 +05:00
ivan
9f4801af68 ++ contour zitadel: sarex-installer machine user for zitadel-setup
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 15:32:27 +05:00
ivan
63b33aaafb ++ 2026-10-29 13:06:42 +05:00
ivan
fb52d422ca ++ 2026-10-29 12:37:46 +05:00
ivan
deb0d5f0eb ++ 2026-10-28 20:06:09 +05:00
1613172dd6 ++ contour kafka client sasl plaintext 2026-10-28 15:47:31 +03:00
b7c36a5348 ++ stand routing layer 2026-10-28 15:31:36 +03:00
0037411aae ++ stand camunda via vault 2026-10-28 15:18:25 +03:00
c23b51e2a5 infra: route GoAlert through VK Istio 2026-10-28 14:39:19 +03:00
448a994fd4 Merge branch 'master' of https://gitlab.sarex.io/infra/iac 2026-10-28 14:38:14 +03:00
Дмитрий Лавренчук
bfad13da2f Merge branch 'pdm/update-image-api-fix-copy' into 'master'
fix: image documentation updated

See merge request infra/iac!14
2026-10-28 10:36:26 +00:00
diamondrigido
f936f5ffb0 fix: image documentation updated 2026-10-28 12:31:00 +02:00
ivan
25b3536124 ++ 2026-10-28 14:51:22 +05:00
ivan
cdc2c34bca ++ 2026-10-28 14:39:15 +05:00
ivan
3b53a99371 ++ 2026-10-28 14:30:36 +05:00
ivan
aa3365cb0d ++ 2026-10-28 14:27:20 +05:00
ivan
7cf8b13f91 ++ 2026-10-28 14:00:45 +05:00
ivan
83bfd4e046 ++ 2026-10-28 13:32:52 +05:00
1c3525fc1b ++ stand platform layer zitadel 2026-10-28 11:15:41 +03:00
a3fa7ba4c0 ++ stand data layer 2026-10-28 10:37:30 +03:00
32e4cff59e ++ stand flux cancel stale health check 2026-10-28 10:33:19 +03:00
4a46bb6626 ++ contour cert-manager images 2026-10-28 10:33:19 +03:00
34cbe7c79e ++ stand mesh layer 2026-10-28 10:33:19 +03:00
ivan
ae911c7d38 ++ 2026-10-28 12:31:10 +05:00
ivan
bd4b6d4ea6 ++ 2026-10-28 12:02:22 +05:00
ivan
56ade56754 ++ 2026-10-28 11:48:59 +05:00
emelinda
8a3540ead7 Add CDE_API_ENABLED environment variable to api and filestream configurations 2026-10-28 08:32:37 +03:00
emelinda
3dadb7a87a Merge branch 'master' of gitlab.sarex.io:infra/iac 2026-10-28 08:30:59 +03:00
emelinda
fc8ddcb511 Add CDE_API_ENABLED environment variable to filestream and api configurations in d8-ugmk-prod 2026-10-28 08:30:40 +03:00
f5a217e793 ++ stand vault layer 2026-10-27 22:48:56 +03:00
eeb27c6e29 ++ slice jobs allow failure 2026-10-27 22:36:55 +03:00
0fb5374e06 ++ slice job per contour 2026-10-27 22:35:41 +03:00
0a81938175 ++ publish contour slices 2026-10-27 22:30:13 +03:00
84f2555494 ++ stand cluster and slicer 2026-10-27 22:30:13 +03:00
faa1dd2cac Merge branch 'task/PDM-2628_download_not_work_wb' into 'master'
fix: update image for filestream wb

See merge request infra/iac!13
2026-10-27 18:55:21 +00:00
diamondrigido
f4a19774a2 fix: update image for filestream wb 2026-10-27 20:11:28 +02:00
emelinda
f39a907a87 [Message]
Update `d8-ugmk-prod` frontend image to `workspaces-v2-frontend:contour_1b5d64e4` in patch configuration
2026-10-27 21:07:58 +03:00
c401cf0dcb Merge branch 'pdm/update_pdf_markings' into 'master'
fix: update pdf markings amqp

See merge request infra/iac!12
2026-10-27 14:16:42 +00:00
diamondrigido
1d349b1a8c fix: move bundle in documentations 2026-10-27 15:47:58 +02:00
ivan
f6d4c2face ++ 2026-10-27 18:07:06 +05:00
diamondrigido
6fda7834ce fix: incorect in asterus pdf markings amqp 2026-10-27 14:49:27 +02:00
diamondrigido
0d4befa985 fix: update pdf markings amqp 2026-10-27 14:48:29 +02:00
ivan
9f263a97f3 ++ 2026-10-27 16:51:18 +05:00
b6f164f85e ++ outline db secret from tf 2026-10-27 13:37:58 +03:00
5e10d54d67 ++ outline pg sslmode 2026-10-27 13:37:58 +03:00
f190a6efa5 Merge branch 'pdm/fix_env_and_doc' into 'master'
fix: output_topic

See merge request infra/iac!10
2026-10-27 10:01:08 +00:00
5bccce5815 Merge branch 'feature/outline-vk-infra' into 'master'
Outline в vk-infra

See merge request infra/iac!11
2026-10-27 09:36:50 +00:00
0c696d3a3e ++ valkey image override 2026-10-27 12:31:57 +03:00
Pavel Ershov
385abf1092
Outline в vk-infra
- infrastructure/outline/vk-infra: namespace, HelmRelease outline-valkey (valkey 0.1.0, standalone) и outline (outline 0.1.0, Outline 1.12.0)
- clusters/vk-infra: подключение outline/vk-infra
- istio-config/vk-infra: Certificate, Gateway и VirtualService для wiki.sarex.space

Секреты outline и outline-valkey созданы в кластере вручную.
2026-10-26 20:49:39 +03:00
diamondrigido
3f7628cc54 fix: output_topic 2026-10-26 18:35:40 +02:00
2182c09031 Merge branch 'feature/infra-3023-pm-wb-kafka-topics' into 'master'
wb/pm: явно задать KAFKA_TOPICS planning → message-hub-prod (INFRA-3023)

See merge request infra/iac!9
2026-10-26 14:33:35 +00:00
emelinda
e33eaa1db8 Merge branch 'master' of gitlab.sarex.io:infra/iac into feature/infra-3023-pm-wb-kafka-topics
# Conflicts:
#	apps/pm/wb/backend.yaml
#	apps/pm/wb/celery.yaml
2026-10-26 19:32:58 +05:00
emelinda
cb99195564 Merge remote-tracking branch 'origin/master' 2026-10-26 18:11:13 +05:00
emelinda
f5ea728d29 Add Kafka configuration to wb backend and Celery YAML files 2026-10-26 18:11:04 +05:00
f86336741b Merge branch 'pdm/fix_env_and_doc' into 'master'
fix: незначительные изменения

See merge request infra/iac!8
2026-10-26 13:07:53 +00:00
4448188464 wb/pm: pin KAFKA_TOPICS planning -> message-hub-prod (INFRA-3023)
pm on WB published to Kafka topic "pm" (stale .env mounted via subPath),
while message-hub consumes "message-hub-prod". PDF export of KSG hung in
PENDING because get_converted_file never reached message-hub.

Set KAFKA_TOPICS explicitly in backend and celery so env overrides .env,
same as prod, d8-ugmk-prod, uralkal and vad.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01173KBT2kTwhxrzr2t3KutB
2026-10-26 12:49:31 +00:00
diamondrigido
af56004006 fix: незначительные изменения
1. Изменения базового образа для документации, чтоб копирование работало
2. output_navis env для wb engine
2026-10-26 13:49:08 +02:00
af112cfffe ++ srx ui kit pages 2026-10-26 14:42:34 +03:00
ivan
03eb83b362 ++ 2026-10-26 14:09:35 +05:00
ivan
a8716d51b3 ++ 2026-10-26 13:55:03 +05:00
ivan
a71b15ae18 ++ 2026-10-26 13:39:04 +05:00
ivan
a2f6a234cb ++ 2026-10-26 13:35:49 +05:00
ivan
6b5e2b7a1f ++ 2026-10-26 13:26:00 +05:00
110 changed files with 11558 additions and 85 deletions

View File

@ -4,6 +4,8 @@ include:
stages: stages:
- build - build
- slices
- slices-publish
variables: variables:
RELEASE_NAME: iac-offline RELEASE_NAME: iac-offline
@ -33,3 +35,31 @@ build_offline_mirror:
- docker push ${IMAGE_NAME} - docker push ${IMAGE_NAME}
- docker push ${CR}/${RELEASE_NAME}:latest - docker push ${CR}/${RELEASE_NAME}:latest
timeout: 30m timeout: 30m
generate_slice_jobs:
stage: slices
image: ${CR}/infra/slice-builder:1.0.0
tags:
- build
rules:
- if: $CI_COMMIT_TAG =~ /^v?(?:\d+\.){2}\d+$/
variables:
GIT_DEPTH: "1"
script:
- python3 ci/publish-slice.py generate --release "$CI_COMMIT_TAG" --commit "$CI_COMMIT_SHA" --out slices-pipeline.yml
- cat slices-pipeline.yml
artifacts:
paths:
- slices-pipeline.yml
publish_contour_slices:
stage: slices-publish
rules:
- if: $CI_COMMIT_TAG =~ /^v?(?:\d+\.){2}\d+$/
needs:
- generate_slice_jobs
trigger:
include:
- artifact: slices-pipeline.yml
job: generate_slice_jobs
strategy: depend

View File

@ -25,11 +25,13 @@
| Путь | Что | | Путь | Что |
|---|---| |---|---|
| `clusters/` | 10 точек входа Flux — **источник истины «что где раскатано»** | | `clusters/` | 17 точек входа Flux + шаблон `contour` — **источник истины «что где раскатано»** |
| `infrastructure/` | 36 платформенных компонентов | | `infrastructure/` | 44 платформенных компонента |
| `apps/` | 37 прикладных сервисов | | `apps/` | 39 прикладных сервисов |
| `docs/apps/` | 35 mermaid-диаграмм по сервисам + `README.md` с доменной группировкой | | `docs/apps/` | 34 mermaid-диаграммы по сервисам + `README.md` с доменной группировкой |
| `docs/architecture/` | модель контура в ArchiMate + скрипты-сканеры (`scan_calls.py`, `scan_contour.py`) и сборка (`build_archimate.py`); реальные значения инсталляций сюда не кладутся, только `*.example.json` |
| `docs/closed-contour-deployment.md` | ранбук поднятия закрытого контура | | `docs/closed-contour-deployment.md` | ранбук поднятия закрытого контура |
| `ci/`, `.gitlab-ci.yml`, `Dockerfile` | на тег `vX.Y.Z`: образ офлайн-зеркала `iac-offline` и публикация «слайсов» контуров (см. ниже) |
| `README.md` | mermaid-мегакарта платформы, структура репозитория, инструкции по добавлению app / infra / кластера | | `README.md` | mermaid-мегакарта платформы, структура репозитория, инструкции по добавлению app / infra / кластера |
| `IAC-CATALOG.md` | каталог всего `infra/*`; §5 — про этот репозиторий | | `IAC-CATALOG.md` | каталог всего `infra/*`; §5 — про этот репозиторий |
| `inventory.yaml` | сгенерированный снимок прода: 59 namespace → kind → имена | | `inventory.yaml` | сгенерированный снимок прода: 59 namespace → kind → имена |
@ -38,15 +40,18 @@
**Кластер.** `clusters/<cluster>/kustomization.yaml` — плоский список `../../apps/<app>/<cluster>`. Инфраструктура подключается двумя разными способами: **Кластер.** `clusters/<cluster>/kustomization.yaml` — плоский список `../../apps/<app>/<cluster>`. Инфраструктура подключается двумя разными способами:
- `d8-ugmk-prod` — прямо в корневом kustomization как `../../infrastructure/<comp>/d8-ugmk-prod`; - `d8-ugmk-prod`, `sarex-contour`, `ugok`, `uralkal`, `vad` — прямо в корневом kustomization как `../../infrastructure/<comp>/<c>`;
- `brusnika-prod`, `brusnika-stage` — через `clusters/<c>/infrastructure/kustomization.yaml`, который ссылается на оверлеи `../../../infrastructure/<comp>/<c>` (`base` + патч HelmRelease + сопутствующие манифесты компонента); - `brusnika-prod`, `brusnika-stage`, `vk-infra`, `vk-prod` — через `clusters/<c>/infrastructure/kustomization.yaml`, который ссылается на оверлеи `../../../infrastructure/<comp>/<c>` (`base` + патч HelmRelease + сопутствующие манифесты компонента);
- `wb` — через `clusters/wb/infrastructure/kustomization.yaml`, который ссылается на `../../../infrastructure/<comp>` (резолвится в `base`), а различия лежат в `clusters/wb/infrastructure/patches/*.yaml`. - `wb`, `yc-cps-prod`, `yc-infra-prod`, `yc-k8s-test-02` — через `clusters/<c>/infrastructure/kustomization.yaml`, который ссылается на `../../../infrastructure/<comp>` (резолвится в `base`), а различия лежат в `clusters/<c>/infrastructure/patches/*.yaml`;
- `stand` — послойно: `layers.yaml` объявляет Flux-`Kustomization` `00-vault → 10-mesh → 20-data → 30-platform → 40-routing` (каталоги `clusters/stand/<NN>-<layer>/`). Слои ссылаются на оверлеи `infrastructure/<comp>/contour-template`, а значения контура (`${STORAGE_CLASS}`, `${HOST_*}`, `${GATEWAY_REPLICAS}`) Flux подставляет через `postBuild.substituteFrom` из ConfigMap `contour-vars`.
Кластеры: `brusnika-prod`, `brusnika-stage`, `d8-ugmk-prod`, `wb`, `yc-cps-prod`, `yc-ecp`, `yc-infra-prod`, `yc-k8s-test`, `yc-k8s-test-02`, `contour`. Кластеры с приложениями: `asterus`, `brusnika-prod`, `brusnika-stage`, `d8-ugmk-prod`, `sarex-contour`, `ugok`, `uralkal`, `vad`, `wb`, `yc-ecp`, `yc-k8s-test`. Только инфраструктура: `stand`, `vk-infra`, `vk-prod`, `yc-cps-prod`, `yc-infra-prod`, `yc-k8s-test-02`. Шаблон: `contour`.
**Приложение.** `apps/<app>/base/` — по одному `.yaml` на процесс (`backend.yaml`, `celery.yaml`, `frontend.yaml`) плюс `namespace.yaml`; оверлеи в `apps/<app>/<cluster>/`. Namespace = имя приложения. Рядом с манифестами лежит документация: `CONFIGURATION.md` (все env-переменные и откуда берутся), `ENDPOINTS.md` (исходящие HTTP-вызовы), `openapi.yaml` (входящий контракт), `.env.example`. Если процессов несколько — файлы с префиксом процесса: `api.ENDPOINTS.md`, `pdm.CONFIGURATION.md`, `frontend.CONFIGURATION.md`. **Приложение.** `apps/<app>/base/` — по одному `.yaml` на процесс (`backend.yaml`, `celery.yaml`, `frontend.yaml`) плюс `namespace.yaml`; оверлеи в `apps/<app>/<cluster>/`. Namespace = имя приложения. Рядом с манифестами лежит документация: `CONFIGURATION.md` (все env-переменные и откуда берутся), `ENDPOINTS.md` (исходящие HTTP-вызовы), `openapi.yaml` (входящий контракт), `.env.example`. Если процессов несколько — файлы с префиксом процесса: `api.ENDPOINTS.md`, `pdm.CONFIGURATION.md`, `frontend.CONFIGURATION.md`.
**Инфра-компонент.** `infrastructure/<comp>/base/` + оверлеи `<comp>/<cluster>/`. Корневой `infrastructure/<comp>/kustomization.yaml` всегда указывает просто на `base`. Чарты идут с суффиксом контура: `vault-contour`, `keycloak-contour`, `postgresql-contour` и — внимание — `idp-contour` это Zitadel. **Инфра-компонент.** `infrastructure/<comp>/base/` + оверлеи `<comp>/<cluster>/`. Корневой `infrastructure/<comp>/kustomization.yaml` всегда указывает просто на `base`. Оверлей `contour-template` (есть у vault, istio-*, cert-manager, postgresql, kafka, rabbitmq, zitadel, camunda, istio-config) — параметризованный `${VAR}`, а не привязанный к конкретному кластеру. Чарты идут с суффиксом контура: `vault-contour`, `keycloak-contour`, `postgresql-contour` и — внимание — `idp-contour` это Zitadel.
**Слайсы.** На тег `vX.Y.Z` `ci/publish-slice.py generate` создаёт child-pipeline с отдельной джобой на каждый `clusters/*/kustomization.yaml`. Джоба вызывает `ci/build-slice.py`: тот обходит дерево Kustomize от `clusters/<c>` и упаковывает только нужные файлы в tar. Результат публикуется как OCI-артефакт `cr.yandex/crpct2b2lsmao4nm0cjk/<c>-iac:<tag>` (media type `application/vnd.sarex.slice.config.v1+json`). Так закрытый контур получает только свой срез. Тесты — `ci/tests/test_build_slice.py`.
## Где искать конкретное ## Где искать конкретное
@ -62,18 +67,23 @@
| Тег образа в контуре | `apps/<app>/<cluster>/*.yaml`, поле `image.tag` | | Тег образа в контуре | `apps/<app>/<cluster>/*.yaml`, поле `image.tag` |
| Что реально в проде по namespace | `inventory.yaml` | | Что реально в проде по namespace | `inventory.yaml` |
| Где лежит исходный код приложения | `IAC-CATALOG.md` §5.3.8 | | Где лежит исходный код приложения | `IAC-CATALOG.md` §5.3.8 |
| Что попадёт в слайс контура | `python3 ci/build-slice.py` по `clusters/<c>` (логика сбора — там же) |
| Переменные шаблонного контура | `${...}` в `infrastructure/*/contour-template`; значения — ConfigMap `contour-vars` в кластере, не в репозитории |
## Ловушки ## Ловушки
- **`dsinv/`** — оверлей есть у 25 приложений, но `clusters/dsinv/` не существует. Это материализованный снимок живого контура WB, а не источник деплоя. Развёрнутым не считать. - **`dsinv/`** — оверлей есть у 25 приложений, но `clusters/dsinv/` не существует. Это материализованный снимок живого контура WB, а не источник деплоя. Развёрнутым не считать.
- **Две несовместимые конвенции оверлеев** (`IAC-CATALOG.md` §5.3.4). `d8-ugmk-prod`, `yc-k8s-test`, `dsinv` наследуют `base` и патчат его. `brusnika-prod`, `brusnika-stage`, `yc-ecp` — полные копии HelmRelease, которые молча дрейфуют от base. Правка `base` на них не влияет. - **Две несовместимые конвенции оверлеев** (`IAC-CATALOG.md` §5.3.4). `d8-ugmk-prod`, `yc-k8s-test`, `vad`, `uralkal`, `sarex-contour`, `dsinv` наследуют `base` и патчат его. `brusnika-prod`, `brusnika-stage`, `yc-ecp`, `asterus` — полные копии HelmRelease, которые молча дрейфуют от base. Правка `base` на них не влияет. **`wb` (4 из 27) и `ugok` (9 из 33) смешанные** — проверяйте `resources:` конкретного оверлея; в `ugok` причина обычно указана комментарием в его `kustomization.yaml`.
- **`asterus` подключён почти пустым**: оверлеи есть у 26 приложений, но в `clusters/asterus/kustomization.yaml` активен только `auth-flow`, остальные закомментированы. Инфраструктуры в кластере нет.
- **Слайс публикуется для каждого `clusters/*/kustomization.yaml`** — в том числе для тестовых и инфраструктурных кластеров. Новая папка в `clusters/` с этим файлом автоматически станет слайсом на следующем теге. У `contour` такого файла нет, поэтому для него слайс не собирается.
- **`IAC-CATALOG.md` — снимок на 05.08.2026** и по составу приложений на контур уже расходится с реальностью (там у `wb` ноль приложений, фактически 26 оверлеев). Всегда сверяться с `clusters/*/kustomization.yaml`. - **`IAC-CATALOG.md` — снимок на 05.08.2026** и по составу приложений на контур уже расходится с реальностью (там у `wb` ноль приложений, фактически 26 оверлеев). Всегда сверяться с `clusters/*/kustomization.yaml`.
- **Мегадиаграмма в `README.md` идеализирована** — рисована руками, показывает все приложения на одной Postgres и все фронтенды за `control-interface`. Проверяемые источники — env-переменные и istio-config. - **Мегадиаграмма в `README.md` идеализирована** — рисована руками, показывает все приложения на одной Postgres и все фронтенды за `control-interface`. Проверяемые источники — env-переменные и istio-config.
- **`clusters/contour/` — не кластер, а шаблон**: в нём нет `flux-system`, только `apps.yaml`, `infrastructure.yaml`, `helm-repositories.yaml`. - **`clusters/contour/` — не кластер, а шаблон**: в нём нет `flux-system`, только `apps.yaml`, `infrastructure.yaml`, `helm-repositories.yaml`.
- **Path-роутинг виден в репозитории только у `d8-ugmk-prod` и `yc-ecp`.** В Brusnika и WB весь прикладной трафик уходит одним правилом в `nginx-service.global-ingress` / `yet-another-nginx-service.global-ingress`, а разводка по путям происходит вне репозитория. Через Istio там публикуются только платформенные UI (Gitea, Grafana, OpenObserve, Superset, Vault, Zitadel, Camunda, MinIO). - **Path-роутинг лежит в `infrastructure/istio-config/<c>/` у большинства контуров, но не у WB.** В `clusters/wb/infrastructure/patches/istio-config.yaml` через Istio публикуются только платформенные UI (Gitea, Grafana, OpenObserve, Superset, Vault, GoAlert), а прикладной трафик разводится вне репозитория. В Brusnika схема смешанная: часть путей идёт напрямую в сервисы, остальное — одним правилом в `yet-another-nginx-service.global-ingress`.
- **Имена namespace расходятся между источниками**: `inventory.yaml` использует суффикс (`flows-prod`), `apps/` — голое имя (`flows`). - **`vk-prod` маршрутизирует в namespace с суффиксом `-prod`** (`eav-prod`, `pm-prod`, `inspections-prod`), а приложений из `apps/` в этом кластере нет: сервисы раскатываются не этим репозиторием.
- **17 оверлеев приложений не подключены ни к одному кластеру** — мёртвый код. - **Имена namespace расходятся между источниками**: `inventory.yaml` и `vk-prod` используют суффикс (`flows-prod`), `apps/` — голое имя (`flows`).
- **Нет диаграмм** в `docs/apps/` для `iam`, `auth-flow`, `ams-sync`. - **66 оверлеев приложений не подключены ни к одному кластеру**: 25 — `dsinv`, 25 — закомментированные в `asterus`, остальные 16 — мёртвый код (в основном `brusnika-prod`, плюс `bi`/`iam` в `wb` и `yc-k8s-test`).
- **Нет диаграмм** в `docs/apps/` для `ams-sync`, `auth-flow`, `bi`, `iam`, `premises`.
- **Чистые фронтенды без бэкенда в этом репозитории**: `auth-flow`, `control-interface`, `cross-section`, `document-link`, `prescriptions`, `projects`, `remarks`, `reviews`, `stamp-verification`. Бэкенд `projects` живёт в `planning/projects-backend`. - **Чистые фронтенды без бэкенда в этом репозитории**: `auth-flow`, `control-interface`, `cross-section`, `document-link`, `prescriptions`, `projects`, `remarks`, `reviews`, `stamp-verification`. Бэкенд `projects` живёт в `planning/projects-backend`.
- Самые центральные сервисы по числу входящих ссылок: `documentations` (~85), `processing` / `workflow` (~76), `django` (~53), `eav` (~47), `resources` (~44), `bim` (~39), `flows` (~38). Менять их конфигурацию — дороже всего. - Самые центральные сервисы по числу входящих ссылок: `documentations` (~85), `processing` / `workflow` (~76), `django` (~53), `eav` (~47), `resources` (~44), `bim` (~39), `flows` (~38). Менять их конфигурацию — дороже всего.

View File

@ -18,7 +18,7 @@ spec:
spec: spec:
containers: containers:
- name: debug - name: debug
image: cr.yandex/crp3ccidau046kdj8g9q/mc-files:dev9 image: cr.yandex/crp3ccidau046kdj8g9q/mc-files:dev5
imagePullPolicy: IfNotPresent imagePullPolicy: IfNotPresent
command: ["/bin/sh", "-c", "while true; do sleep 3600; done"] command: ["/bin/sh", "-c", "while true; do sleep 3600; done"]
imagePullSecrets: imagePullSecrets:

View File

@ -8,6 +8,9 @@ spec:
values: values:
services: services:
frontend: frontend:
image:
name:
_default: cr.yandex/crp3ccidau046kdj8g9q/cross-section-app:contour_c5e5d476@sha256:04e0eb23df7138eb6c0dad6d790c7229607cb70eb7a4d7448aa70a236caa7d52
deployment: deployment:
resources: resources:
requests: requests:

View File

@ -8,6 +8,9 @@ spec:
values: values:
services: services:
backend: backend:
deployment:
replicaCount:
_default: 2
envs: envs:
- name: ALLOWED_HOSTS - name: ALLOWED_HOSTS
value: value:
@ -129,10 +132,6 @@ spec:
- name: ZITADEL_HOST - name: ZITADEL_HOST
value: value:
_default: https://sarex-login.uralmine.com _default: https://sarex-login.uralmine.com
# Включено вместе с message-hub: он консьюмер топика message-hub-stage
# (SETTINGS_TOPICS в apps/message-hub/d8-ugmk-prod/message-hub.yaml),
# а писать в этот топик должен django. Vault-шаблон django-kafka
# и KAFKA_SSL_CAFILE уже есть в base — здесь только флаг.
- name: SERVER_KAFKA_ENABLED - name: SERVER_KAFKA_ENABLED
value: value:
_default: "True" _default: "True"

File diff suppressed because one or more lines are too long

View File

@ -36,7 +36,7 @@ spec:
image: image:
name: name:
_default: cr.yandex/crp3ccidau046kdj8g9q/pdf-markings-amqp:prod_3ab263be _default: cr.yandex/crp3ccidau046kdj8g9q/pdf-markings-amqp:prod_9dea5b64
pullPolicy: pullPolicy:
_default: IfNotPresent _default: IfNotPresent

View File

@ -65,7 +65,7 @@ spec:
enabled: false enabled: false
image: image:
name: name:
_default: cr.yandex/crp3ccidau046kdj8g9q/documentations:prod_179e518c _default: cr.yandex/crp3ccidau046kdj8g9q/documentations:prod_89c8039a
pullPolicy: pullPolicy:
_default: IfNotPresent _default: IfNotPresent
service: service:
@ -89,6 +89,9 @@ spec:
- name: POSTGRES_POOL_SIZE - name: POSTGRES_POOL_SIZE
value: value:
_default: "20" _default: "20"
- name: CDE_API_ENABLED
value:
_default: "false"
- name: USE_LEGACY_BIM_FLOW - name: USE_LEGACY_BIM_FLOW
value: value:
_default: "true" _default: "true"
@ -236,6 +239,9 @@ spec:
- name: VALKEY_PORT - name: VALKEY_PORT
value: value:
_default: "6379" _default: "6379"
- name: CDE_API_ENABLED
value:
_default: "false"
podAnnotations: podAnnotations:
_default: _default:
traffic.sidecar.istio.io/excludeOutboundPorts: "8200" traffic.sidecar.istio.io/excludeOutboundPorts: "8200"

View File

@ -97,6 +97,9 @@ spec:
- name: PUBLIC_LINK_FOLDER_CONNECTOR_ENABLED - name: PUBLIC_LINK_FOLDER_CONNECTOR_ENABLED
value: value:
_default: "true" _default: "true"
- name: CDE_API_ENABLED
value:
_default: "false"
- name: USE_ZITADEL - name: USE_ZITADEL
value: value:
_default: "1" _default: "1"
@ -223,6 +226,9 @@ spec:
- name: VALKEY_PORT - name: VALKEY_PORT
value: value:
_default: "6379" _default: "6379"
- name: CDE_API_ENABLED
value:
_default: "false"
podAnnotations: podAnnotations:
_default: _default:
traffic.sidecar.istio.io/excludeOutboundPorts: "8200" traffic.sidecar.istio.io/excludeOutboundPorts: "8200"

View File

@ -37,7 +37,7 @@ spec:
name: name:
_default: pdf-markings-amqp _default: pdf-markings-amqp
replicaCount: replicaCount:
_default: 1 _default: 2
port: port:
_default: 8000 _default: 8000
command: command:
@ -64,7 +64,7 @@ spec:
enabled: false enabled: false
image: image:
name: name:
_default: cr.yandex/crp3ccidau046kdj8g9q/pdf-markings-amqp:prod_3ab263be _default: cr.yandex/crp3ccidau046kdj8g9q/pdf-markings-amqp:prod_9dea5b64
pullPolicy: pullPolicy:
_default: IfNotPresent _default: IfNotPresent
service: service:

View File

@ -156,3 +156,6 @@ spec:
- name: VALKEY_PORT - name: VALKEY_PORT
value: value:
_default: "6379" _default: "6379"
- name: CDE_API_ENABLED
value:
_default: "false"

View File

@ -144,3 +144,6 @@ spec:
- name: VALKEY_PORT - name: VALKEY_PORT
value: value:
_default: "6379" _default: "6379"
- name: CDE_API_ENABLED
value:
_default: "false"

View File

@ -8,6 +8,10 @@ spec:
values: values:
services: services:
backend: backend:
image:
name:
_default: cr.yandex/crp3ccidau046kdj8g9q/pdf-markings:contour_ce9ed6d7
envs: envs:
- name: MARKS_APP__LOG_LEVEL - name: MARKS_APP__LOG_LEVEL
value: value:
@ -33,8 +37,6 @@ spec:
value: value:
_default: "true" _default: "true"
# Временно: в образе нет CA УГМК для s3.uralmine.com (INFRA-3034).
# Вернуть "true" после монтирования CA и AWS_CA_BUNDLE.
- name: MARKS_S3__SSL_VERIFY - name: MARKS_S3__SSL_VERIFY
value: value:
_default: "false" _default: "false"

View File

@ -36,7 +36,7 @@ spec:
image: image:
name: name:
_default: cr.yandex/crp3ccidau046kdj8g9q/documentations:prod_98a23ef5 _default: cr.yandex/crp3ccidau046kdj8g9q/documentations:prod_0be18f51
pullPolicy: pullPolicy:
_default: IfNotPresent _default: IfNotPresent
@ -135,6 +135,10 @@ spec:
value: value:
_default: "20" _default: "20"
- name: CDE_API_ENABLED
value:
_default: "false"
- name: ZITADEL_ACCOUNT - name: ZITADEL_ACCOUNT
value: value:
_default: "/etc/sarex/zitadel/zitadel-account.json" _default: "/etc/sarex/zitadel/zitadel-account.json"

View File

@ -36,7 +36,7 @@ spec:
image: image:
name: name:
_default: cr.yandex/crp3ccidau046kdj8g9q/documentations-api-files:prod_3f19a21d _default: cr.yandex/crp3ccidau046kdj8g9q/documentations-api-files:prod_fef3add2
pullPolicy: pullPolicy:
_default: IfNotPresent _default: IfNotPresent
@ -138,6 +138,10 @@ spec:
value: value:
_default: "0.0.0.0:8080" _default: "0.0.0.0:8080"
- name: CDE_API_ENABLED
value:
_default: "false"
- name: API_ADDRESS_FILE - name: API_ADDRESS_FILE
value: value:
_default: "0.0.0.0:8080" _default: "0.0.0.0:8080"

View File

@ -36,7 +36,7 @@ spec:
image: image:
name: name:
_default: cr.yandex/crp3ccidau046kdj8g9q/pdf-markings-amqp:prod_3ab263be _default: cr.yandex/crp3ccidau046kdj8g9q/pdf-markings-amqp:prod_9dea5b64
pullPolicy: pullPolicy:
_default: IfNotPresent _default: IfNotPresent

View File

@ -8,6 +8,9 @@ spec:
values: values:
services: services:
backend: backend:
image:
name:
_default: cr.yandex/crp3ccidau046kdj8g9q/eav:prod_0dcebba6@sha256:2630ad45e4704881ac834f48a70ea837fa46620a5339c3e462282fd16399cc9c
deployment: deployment:
resources: resources:
requests: requests:

View File

@ -18,7 +18,7 @@ data:
# DEBUG SETTINGS START # DEBUG SETTINGS START
# --------------------------------------------------------------------------------------------------------------------- # ---------------------------------------------------------------------------------------------------------------------
DEBUG = True DEBUG = False
ALLOWED_HOSTS = ['*'] ALLOWED_HOSTS = ['*']
# --------------------------------------------------------------------------------------------------------------------- # ---------------------------------------------------------------------------------------------------------------------
# DEBUG SETTINGS END # DEBUG SETTINGS END

View File

@ -9,6 +9,8 @@ spec:
services: services:
backend: backend:
deployment: deployment:
replicaCount:
_default: 2
args: args:
_default: _default:
- | - |

View File

@ -8,6 +8,9 @@ spec:
values: values:
services: services:
backend: backend:
image:
name:
_default: cr.yandex/crp3ccidau046kdj8g9q/iams:contour_29754513@sha256:a12cf64b59da50cf99011ee4fc8559e61af637887265f1377a448ee321de2250
envs: envs:
- name: ENVIRONMENT - name: ENVIRONMENT
value: value:

View File

@ -1,33 +1,5 @@
--- ---
# ВНИМАНИЕ: data["production.py"] — одна строка, kustomize заменяет её ЦЕЛИКОМ,
# поэтому здесь продублировано всё содержимое из
# apps/issues/base/production-configmap.yaml (та же ловушка, что с envs).
#
# Зачем нужен этот патч:
# production.py делает `from .base import *` (там всё через os.getenv), а затем
# переприсваивает часть значений ХАРДКОДОМ — поэтому для этих переменных
# ConfigMap ПОБЕЖДАЕТ env из HelmRelease, и правка env в оверлее
# backend-s3.yaml/celery-s3.yaml сама по себе ни на что не влияет.
#
# INFRA-3005 (основная причина зависания экспорта в Excel):
# в base хардкодом WORKFLOWS_HOST/WORKFLOWS_URL = workflows-api-service.workflow
# — такого сервиса в УГМК нет. celery собирает xlsx, затем
# ExportFinishedNotification → issues/mailing.py:122 делает POST на
# {WORKFLOWS_HOST}/internal/v1/companies/{id}/workflows и падает с
# ConnectionError (Name or service not known) — задача фейлится, фронт не
# получает завершения и крутит загрузку бесконечно. Тем же путём с 26.08
# падали ВСЕ уведомления (send_notification_on_create/on_update/
# to_new_responsible/on_bulk_update, ~1770 ошибок).
#
# Отличия от base:
# - WORKFLOWS_HOST / WORKFLOWS_URL → backend-svc.processing (через os.getenv,
# чтобы env из оверлея снова начал работать)
# - SAREX_API → https://sarex.uralmine.com (в base зашит https://srx.wb.ru)
# - CORS/CSRF origins → https://sarex.uralmine.com (в base только lk.srx.wb.ru)
# - убраны хардкоды KAFKA_USERNAME/KAFKA_PASSWORD/DJANGO_TOKEN и чужой
# дефолт KAFKA_HOST: в УГМК всё это приходит из Vault
# (см. podAnnotations в backend-s3.yaml / celery-s3.yaml / kafka-app.yaml)
# - BASE_AERO_URL оставлен внутренним — он server-to-server
apiVersion: v1 apiVersion: v1
kind: ConfigMap kind: ConfigMap
metadata: metadata:
@ -146,9 +118,9 @@ data:
CORS_ALLOW_ALL_ORIGINS = True CORS_ALLOW_ALL_ORIGINS = True
ENABLE_MAILGUN=False ENABLE_MAILGUN=False
SMTP_PORT=465 SMTP_PORT=587
SMTP_HOST="mail.rwb.ru" SMTP_HOST="post.uralmine.com"
EMAIL_FROM="sarex@rwb.ru" EMAIL_FROM="sarex@uralmine.com"
CORS_ALLOW_METHODS = [ CORS_ALLOW_METHODS = [
"DELETE", "DELETE",

View File

@ -12,7 +12,6 @@ data:
# DEBUG SETTINGS START # DEBUG SETTINGS START
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
DEBUG = True
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
TEST_MODE = False TEST_MODE = False
@ -40,7 +39,7 @@ data:
# DEBUG SETTINGS START # DEBUG SETTINGS START
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
DEBUG = True DEBUG = False
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
REVIEW_HOST='http://backend-service.flows.svc.cluster.local:8000' REVIEW_HOST='http://backend-service.flows.svc.cluster.local:8000'

View File

@ -1,11 +1,5 @@
--- ---
# Патч backend для vad.
#
# ВНИМАНИЕ: args — список, kustomize заменяет его ЦЕЛИКОМ (JSON merge patch),
# поэтому здесь продублирован весь скрипт из apps/pm/base/backend.yaml.
# При правке args в base их нужно продублировать и сюда.
# podAnnotations — map, мержится по ключам, поэтому переопределён только
# один ключ agent-inject-template-pm-s3; остальные vault-аннотации берутся из base.
apiVersion: helm.toolkit.fluxcd.io/v2 apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease kind: HelmRelease
metadata: metadata:
@ -15,16 +9,15 @@ spec:
values: values:
services: services:
backend: backend:
deployment:
resources:
requests:
cpu: null
memory: null
image: image:
name: name:
_default: cr.yandex/crp3ccidau046kdj8g9q/pm-backend:production_fa0f1551 _default: cr.yandex/crp3ccidau046kdj8g9q/pm-backend:production_fa0f1551
deployment: deployment:
resources:
requests:
cpu: null
memory: null
args: args:
_default: _default:
- | - |
@ -34,7 +27,6 @@ spec:
[ -f /vault/secrets/pm-s3 ] && . /vault/secrets/pm-s3 [ -f /vault/secrets/pm-s3 ] && . /vault/secrets/pm-s3
[ -f /vault/secrets/pm-kafka ] && . /vault/secrets/pm-kafka [ -f /vault/secrets/pm-kafka ] && . /vault/secrets/pm-kafka
set +a set +a
python manage.py makemigrations pm --noinput
python manage.py migrate python manage.py migrate
exec /opt/sarex/entrypoint.sh exec /opt/sarex/entrypoint.sh
envs: envs:
@ -62,6 +54,10 @@ spec:
value: value:
_default: "/api/v1" _default: "/api/v1"
- name: SETTINGS_VERIFY_SSL
value:
_default: "false"
- name: TRACING_INSECURE - name: TRACING_INSECURE
value: value:
_default: "False" _default: "False"

View File

@ -15,11 +15,6 @@ spec:
values: values:
services: services:
celery: celery:
deployment:
resources:
requests:
cpu: null
memory: null
# Образ общий с backend — держать в одной версии обязательно, # Образ общий с backend — держать в одной версии обязательно,
# иначе воркер разъедется с API по схеме задач и моделям. # иначе воркер разъедется с API по схеме задач и моделям.
image: image:
@ -29,6 +24,10 @@ spec:
# base не переопределяет args, а нам нужно добавить sourcing pm-kafka — # base не переопределяет args, а нам нужно добавить sourcing pm-kafka —
# дублируем скрипт целиком (то же самое, что и в backend.yaml). # дублируем скрипт целиком (то же самое, что и в backend.yaml).
deployment: deployment:
resources:
requests:
cpu: null
memory: null
args: args:
_default: _default:
- | - |

View File

@ -147,6 +147,28 @@ spec:
value: value:
_default: "/api/v1" _default: "/api/v1"
- name: KAFKA_ENABLE
value:
_default: "True"
- name: KAFKA_BOOTSTRAP_SERVERS
value:
_default: '["wb-prod-kafka-bootstrap.kafka.svc.cluster.local:9093"]'
- name: KAFKA_SECURITY_PROTOCOL
value:
_default: "SSL"
- name: KAFKA_SSL_CAFILE
value:
_default: "/usr/local/share/ca-certificates/kafka.crt"
# Обязан совпадать с SETTINGS_TOPICS["planning"] в
# apps/message-hub/wb/message-hub.yaml
- name: KAFKA_TOPICS
value:
_default: '{"planning": "message-hub-prod"}'
commitSha: "" commitSha: ""
gitlabUri: "" gitlabUri: ""
gitlabJobUrl: "" gitlabJobUrl: ""

View File

@ -141,6 +141,26 @@ spec:
value: value:
_default: "/api/v1" _default: "/api/v1"
- name: KAFKA_ENABLE
value:
_default: "True"
- name: KAFKA_BOOTSTRAP_SERVERS
value:
_default: '["wb-prod-kafka-bootstrap.kafka.svc.cluster.local:9093"]'
- name: KAFKA_SECURITY_PROTOCOL
value:
_default: "SSL"
- name: KAFKA_SSL_CAFILE
value:
_default: "/usr/local/share/ca-certificates/kafka.crt"
- name: KAFKA_TOPICS
value:
_default: '{"planning": "message-hub-prod"}'
commitSha: "" commitSha: ""
gitlabUri: "" gitlabUri: ""
gitlabJobUrl: "" gitlabJobUrl: ""

View File

@ -0,0 +1,6 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- namespace.yaml
- postgresql.yaml
- notes-tls.yaml

View File

@ -0,0 +1,5 @@
---
apiVersion: v1
kind: Namespace
metadata:
name: postgres-tls-test

View File

@ -0,0 +1,119 @@
---
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: notes-tls
namespace: postgres-tls-test
spec:
interval: 10m
dependsOn:
- name: pg-tls
chart:
spec:
chart: universal-chart
version: "0.1.9"
sourceRef:
kind: HelmRepository
name: yc-oci-charts
namespace: flux-system
interval: 10m
install:
remediation:
retries: 3
upgrade:
remediation:
retries: 3
values:
global:
env: _default
services:
backend:
enabled: true
deployment:
enabled: true
name:
_default: notes-tls
replicaCount:
_default: 1
port:
_default: 8000
resources:
requests:
cpu:
_default: 25m
memory:
_default: 128Mi
probes:
liveness:
enabled: false
readiness:
enabled: false
image:
name:
_default: cr.yandex/crp3ccidau046kdj8g9q/notes-backend:preprod_c00faf6e
pullPolicy:
_default: IfNotPresent
service:
enabled: true
name:
_default: notes-tls
type:
_default: ClusterIP
port:
_default: 8000
targetPort:
_default: 8000
portName:
_default: http
imagePullSecrets:
enabled:
_default: true
name:
_default: regcred
envs:
- name: PG_HOST
value:
_default: pg-tls.postgres-tls-test.svc.cluster.local
- name: PG_PORT
value:
_default: "5432"
- name: PG_DB
value:
_default: notes
- name: PG_LOGIN
value:
_default: notes
- name: PG_SSL
value:
_default: "true"
- name: PG_SSL_MODE
value:
_default: verify-full
- name: PGSSLMODE
value:
_default: verify-full
- name: PGSSLROOTCERT
value:
_default: /etc/pg-ca/ca.crt
- name: DJANGO_USE
value:
_default: "false"
- name: TIMEOUT
value:
_default: "120"
secretEnvs:
- name: PG_PASSWORD
secretName:
_default: pg-tls
secretKey:
_default: password
volumes:
_default:
- name: pg-ca
mountPath: /etc/pg-ca
readOnly: true
secret:
secretName: pg-tls-crt
items:
- key: ca.crt
path: ca.crt

View File

@ -0,0 +1,88 @@
---
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: pg-tls
namespace: postgres-tls-test
spec:
interval: 10m
timeout: 15m
chart:
spec:
chart: postgresql-contour
version: "17.0.7"
sourceRef:
kind: HelmRepository
name: yc-oci-charts
namespace: flux-system
interval: 10m
install:
remediation:
retries: 3
upgrade:
remediation:
retries: 3
values:
fullnameOverride: pg-tls
global:
security:
allowInsecureImages: true
defaultStorageClass: local-path
imagePullSecrets:
- regcred
image:
registry: cr.yandex/crp3ccidau046kdj8g9q
repository: contour/postgresql
tag: 17.0.7
auth:
username: notes
database: notes
metrics:
enabled: false
tls:
enabled: true
autoGenerated: true
primary:
persistence:
storageClass: local-path
size: 1Gi
nodeSelector:
dedicated: generic
containerSecurityContext:
readOnlyRootFilesystem: false
args:
- postgres
- -c
- ssl=on
- -c
- ssl_cert_file=/opt/bitnami/postgresql/certs/tls.crt
- -c
- ssl_key_file=/opt/bitnami/postgresql/certs/tls.key
- -c
- ssl_min_protocol_version=TLSv1.2
- -c
- hba_file=/bitnami/postgresql/conf/pg_hba.conf
customLivenessProbe:
exec:
command:
- /bin/sh
- -c
- exec pg_isready -U notes -d notes -h 127.0.0.1 -p 5432
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 6
customReadinessProbe:
exec:
command:
- /bin/sh
- -c
- exec pg_isready -U notes -d notes -h 127.0.0.1 -p 5432
initialDelaySeconds: 5
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 6
pgHbaConfiguration: |
local all all scram-sha-256
hostssl all all 0.0.0.0/0 scram-sha-256
hostssl all all ::/0 scram-sha-256

View File

@ -8,6 +8,9 @@ spec:
values: values:
services: services:
frontend: frontend:
image:
name:
_default: cr.yandex/crp3ccidau046kdj8g9q/prescriptions-frontend:production_d48699e6@sha256:1e6925d1a41d91878013eb02e4463c30efa0479c25260575ad4fd3ee5471575d
deployment: deployment:
resources: resources:
requests: requests:

View File

@ -42,7 +42,7 @@ spec:
image: image:
name: name:
_default: cr.yandex/crp3ccidau046kdj8g9q/workflows-engine:contour_89322f69 _default: cr.yandex/crp3ccidau046kdj8g9q/workflows-engine:contour_835b57c1
pullPolicy: pullPolicy:
_default: IfNotPresent _default: IfNotPresent
@ -132,6 +132,20 @@ spec:
value: value:
_default: "0.0.0.0:8000" _default: "0.0.0.0:8000"
- name: ENABLE_CROSS_SECTION_API_DB
value:
_default: "1"
- name: CROSS_SECTION_API_DB
value:
_default: "/etc/cross_section/cross-section-api-db-prod.json"
- name: CROSS_SECTION_API_DEBUG
value:
_default: "0"
- name: IGNORE_RESOURCES
value:
_default: "True"
- name: DJANGO_HOST - name: DJANGO_HOST
value: value:
_default: "http://backend.django.svc.cluster.local:8000" _default: "http://backend.django.svc.cluster.local:8000"

View File

@ -8,6 +8,9 @@ spec:
values: values:
services: services:
backend: backend:
image:
name:
_default: cr.yandex/crp3ccidau046kdj8g9q/workflows-engine:contour_7d2cd329
deployment: deployment:
resources: resources:
requests: requests:

View File

@ -8,6 +8,9 @@ spec:
values: values:
services: services:
frontend: frontend:
image:
name:
_default: cr.yandex/crp3ccidau046kdj8g9q/project-frontend-app:wb_fd49eb19@sha256:09968df4919be603200ccf39662bab15efdc1db902459abf912c6c533c896a11
deployment: deployment:
resources: resources:
requests: requests:

View File

@ -8,6 +8,9 @@ spec:
values: values:
services: services:
frontend: frontend:
image:
name:
_default: cr.yandex/crp3ccidau046kdj8g9q/remarks-frontend:contour_013c5091@sha256:05b8667b934ac785a48a41b20fcbd7e4e5b6f377f29a6e47add9c23c474a42c7
deployment: deployment:
resources: resources:
requests: requests:

View File

@ -8,6 +8,9 @@ spec:
values: values:
services: services:
frontend: frontend:
image:
name:
_default: cr.yandex/crp3ccidau046kdj8g9q/reviews-frontend:contour_c5a6d3b4@sha256:4333dbd7a7a68dcf4465563d8e079ee29fd52750c2bc841f92e745fb19e84591
deployment: deployment:
resources: resources:
requests: requests:

View File

@ -29,7 +29,7 @@ spec:
enabled: true enabled: true
image: image:
name: name:
_default: cr.yandex/crp3ccidau046kdj8g9q/workspaces-v2-frontend:contour_a72e3265 _default: cr.yandex/crp3ccidau046kdj8g9q/workspaces-v2-frontend:contour_2da2a00e
pullPolicy: pullPolicy:
_default: IfNotPresent _default: IfNotPresent
deployment: deployment:

View File

@ -8,6 +8,9 @@ spec:
values: values:
services: services:
frontend: frontend:
image:
name:
_default: cr.yandex/crp3ccidau046kdj8g9q/workspaces-v2-frontend:contour_1b5d64e4
deployment: deployment:
replicaCount: replicaCount:
_default: 1 _default: 1

View File

@ -36,7 +36,7 @@ spec:
image: image:
name: name:
_default: 10.4.10.187:80/library/workspaces-v2-frontend:contour_07373a45 _default: 10.4.10.187:80/library/workspaces-v2-frontend:contour_1b5d64e4
pullPolicy: pullPolicy:
_default: IfNotPresent _default: IfNotPresent

163
ci/build-slice.py Normal file
View File

@ -0,0 +1,163 @@
#!/usr/bin/env python3
import argparse
import io
import subprocess
import sys
import tarfile
from pathlib import Path, PurePosixPath
import yaml
FLUX_KUSTOMIZATION = "kustomize.toolkit.fluxcd.io"
KUSTOMIZATION_FILES = ("kustomization.yaml", "kustomization.yml", "Kustomization")
class SliceError(Exception):
pass
def is_remote(ref: str) -> bool:
return "://" in ref or ref.startswith(("git@", "github.com/", "gitlab.com/"))
def kustomization_file(directory: Path) -> Path | None:
for name in KUSTOMIZATION_FILES:
candidate = directory / name
if candidate.is_file():
return candidate
return None
def local_refs(doc: dict) -> list[str]:
refs = []
refs += doc.get("resources") or []
refs += doc.get("components") or []
refs += doc.get("patchesStrategicMerge") or []
refs += [p["path"] for p in doc.get("patches") or [] if isinstance(p, dict) and "path" in p]
for key in ("configMapGenerator", "secretGenerator"):
for gen in doc.get(key) or []:
for entry in (gen.get("files") or []) + (gen.get("envs") or []):
refs.append(entry.split("=", 1)[-1])
if gen.get("env"):
refs.append(gen["env"])
return [r for r in refs if isinstance(r, str)]
def flux_paths(path: Path) -> list[str]:
try:
docs = list(yaml.safe_load_all(path.read_text()))
except yaml.YAMLError:
return []
return [
d["spec"]["path"]
for d in docs
if isinstance(d, dict)
and str(d.get("apiVersion", "")).startswith(FLUX_KUSTOMIZATION)
and d.get("kind") == "Kustomization"
and d.get("spec", {}).get("path")
]
def collect(repo: Path, start: str) -> tuple[set[PurePosixPath], list[str]]:
repo = repo.resolve()
files: set[PurePosixPath] = set()
entries: list[str] = []
seen_dirs: set[Path] = set()
def rel(p: Path) -> PurePosixPath:
try:
return PurePosixPath(p.resolve().relative_to(repo).as_posix())
except ValueError:
raise SliceError(f"reference outside the repository: {p}")
def add_file(p: Path):
r = rel(p)
if r in files:
return
files.add(r)
if p.suffix in (".yaml", ".yml"):
for fp in flux_paths(p):
visit_dir(repo / fp, entry=True)
def visit_dir(d: Path, entry: bool = False):
d = d.resolve()
if d in seen_dirs:
return
seen_dirs.add(d)
kfile = kustomization_file(d)
if kfile is None:
raise SliceError(f"no kustomization.yaml in {rel(d)}")
if entry:
entries.append(str(rel(d)))
add_file(kfile)
doc = yaml.safe_load(kfile.read_text()) or {}
for ref in local_refs(doc):
if is_remote(ref):
raise SliceError(f"remote reference {ref} in {rel(kfile)}")
target = (d / ref)
if target.is_dir():
visit_dir(target)
elif target.is_file():
add_file(target)
else:
raise SliceError(f"reference {ref} in {rel(kfile)} does not exist")
visit_dir(repo / start, entry=True)
return files, sorted(set(entries))
def write_tar(repo: Path, files: set[PurePosixPath], out: Path):
buf = io.BytesIO()
with tarfile.open(fileobj=buf, mode="w", format=tarfile.PAX_FORMAT) as tar:
for f in sorted(files):
data = (repo / f).read_bytes()
info = tarfile.TarInfo(str(f))
info.size = len(data)
info.mode = 0o644
info.mtime = 0
info.uid = info.gid = 0
info.uname = info.gname = ""
tar.addfile(info, io.BytesIO(data))
out.write_bytes(buf.getvalue())
def kustomize_build(path: Path) -> bytes:
p = subprocess.run(["kubectl", "kustomize", str(path)], capture_output=True)
if p.returncode != 0:
raise SliceError(f"kustomize build {path} failed: {p.stderr.decode().strip()}")
return p.stdout
def verify(repo: Path, out: Path, entries: list[str]):
import tempfile
with tempfile.TemporaryDirectory() as tmp:
with tarfile.open(out) as tar:
tar.extractall(tmp, filter="data")
for e in entries:
if kustomize_build(repo / e) != kustomize_build(Path(tmp) / e):
raise SliceError(f"kustomize build of {e} differs between slice and full repository")
def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument("contour")
ap.add_argument("--repo", default=".")
ap.add_argument("--out", required=True)
ap.add_argument("--no-verify", action="store_true")
args = ap.parse_args()
repo = Path(args.repo).resolve()
out = Path(args.out)
try:
files, entries = collect(repo, f"clusters/{args.contour}")
write_tar(repo, files, out)
if not args.no_verify:
verify(repo, out, entries)
except SliceError as e:
print(f"slice {args.contour}: {e}", file=sys.stderr)
return 1
print(f"slice {args.contour}: {len(files)} files, entry points: {', '.join(entries)}")
return 0
if __name__ == "__main__":
sys.exit(main())

217
ci/publish-slice.py Normal file
View File

@ -0,0 +1,217 @@
#!/usr/bin/env python3
import argparse
import base64
import hashlib
import importlib.util
import json
import re
import sys
import tempfile
import time
import urllib.error
import urllib.parse
import urllib.request
from datetime import datetime, timezone
from pathlib import Path
import yaml
spec = importlib.util.spec_from_file_location("build_slice", Path(__file__).with_name("build-slice.py"))
bs = importlib.util.module_from_spec(spec)
spec.loader.exec_module(bs)
REGISTRY_HOST = "cr.yandex"
SLICE_REGISTRY = "crpct2b2lsmao4nm0cjk"
CONFIG_TYPE = "application/vnd.sarex.slice.config.v1+json"
LAYER_TYPE = "application/vnd.oci.image.layer.v1.tar"
MANIFEST_TYPE = "application/vnd.oci.image.manifest.v1+json"
JOB_IMAGE = "${CR}/infra/slice-builder:1.0.0"
class PublishError(Exception):
pass
def digest(data: bytes) -> str:
return "sha256:" + hashlib.sha256(data).hexdigest()
def list_contours(repo: Path) -> list[str]:
return sorted(p.parent.name for p in repo.glob("clusters/*/kustomization.yaml"))
def child_pipeline(contours: list[str], release: str, commit: str) -> str:
jobs: dict = {"stages": ["slices"]}
for c in contours:
jobs[f"slice {c}"] = {
"stage": "slices",
"image": JOB_IMAGE,
"tags": ["build"],
"allow_failure": True,
"variables": {"GIT_DEPTH": "1"},
"script": [
"printf '%s' \"$SLICES_PUSHER_KEY\" | base64 -d > /tmp/slices-key.json",
f"python3 ci/publish-slice.py publish --contour {c} --release {release} --commit {commit} --key-file /tmp/slices-key.json",
],
"after_script": ["rm -f /tmp/slices-key.json"],
}
if not contours:
jobs["no contours"] = {"stage": "slices", "image": JOB_IMAGE, "tags": ["build"], "script": ["echo 'no clusters/*/kustomization.yaml found'"]}
return yaml.safe_dump(jobs, sort_keys=False, allow_unicode=False)
def build_manifest(layer: bytes, annotations: dict[str, str]) -> tuple[bytes, bytes]:
config = b"{}"
manifest = {
"schemaVersion": 2,
"mediaType": MANIFEST_TYPE,
"config": {"mediaType": CONFIG_TYPE, "digest": digest(config), "size": len(config)},
"layers": [{
"mediaType": LAYER_TYPE,
"digest": digest(layer),
"size": len(layer),
"annotations": {"org.opencontainers.image.title": "slice.tar"},
}],
"annotations": dict(sorted(annotations.items())),
}
return config, json.dumps(manifest, sort_keys=True, separators=(",", ":")).encode()
class Registry:
def __init__(self, username: str, password: str):
self.base = f"https://{REGISTRY_HOST}"
self.basic = "Basic " + base64.b64encode(f"{username}:{password}".encode()).decode()
self.tokens: dict[str, str] = {}
def _send(self, method, url, data=None, headers=None):
req = urllib.request.Request(url, data=data, method=method, headers=headers or {})
try:
with urllib.request.urlopen(req, timeout=60) as r:
return r.status, dict(r.headers), r.read()
except urllib.error.HTTPError as e:
return e.code, dict(e.headers), e.read()
def _token(self, challenge: str, scope: str) -> str:
params = dict(re.findall(r'(\w+)="([^"]*)"', challenge))
realm = params.pop("realm")
params["scope"] = scope
status, _, body = self._send("GET", f"{realm}?{urllib.parse.urlencode(params)}", headers={"Authorization": self.basic})
if status != 200:
raise PublishError(f"registry auth failed: HTTP {status}")
payload = json.loads(body)
return payload.get("token") or payload.get("access_token")
def request(self, method, repo, path, data=None, headers=None):
scope = f"repository:{repo}:pull,push"
url = path if path.startswith("http") else f"{self.base}/v2/{repo}/{path}"
headers = dict(headers or {})
if scope in self.tokens:
headers["Authorization"] = f"Bearer {self.tokens[scope]}"
status, h, body = self._send(method, url, data, headers)
if status == 401:
challenge = {k.lower(): v for k, v in h.items()}.get("www-authenticate", "")
self.tokens[scope] = self._token(challenge, scope)
headers["Authorization"] = f"Bearer {self.tokens[scope]}"
status, h, body = self._send(method, url, data, headers)
return status, {k.lower(): v for k, v in h.items()}, body
def current_layer(self, repo: str) -> str | None:
status, _, body = self.request("GET", repo, "manifests/current", headers={"Accept": MANIFEST_TYPE})
if status == 404:
return None
if status != 200:
raise PublishError(f"{repo}:current: HTTP {status}")
return json.loads(body)["layers"][0]["digest"]
def put_blob(self, repo: str, data: bytes) -> bool:
d = digest(data)
if self.request("HEAD", repo, f"blobs/{d}")[0] == 200:
return False
status, h, _ = self.request("POST", repo, "blobs/uploads/")
if status != 202:
raise PublishError(f"blob upload to {repo}: HTTP {status}")
location = h["location"]
if location.startswith("/"):
location = self.base + location
sep = "&" if "?" in location else "?"
status, _, _ = self.request("PUT", repo, f"{location}{sep}digest={d}", data, {"Content-Type": "application/octet-stream"})
if status != 201:
raise PublishError(f"blob upload to {repo}: HTTP {status}")
return True
def push(self, repo: str, tags: list[str], layer: bytes, annotations: dict[str, str]) -> bool:
config, manifest = build_manifest(layer, annotations)
self.put_blob(repo, config)
uploaded = self.put_blob(repo, layer)
for tag in tags:
status, _, body = self.request("PUT", repo, f"manifests/{tag}", manifest, {"Content-Type": MANIFEST_TYPE})
if status != 201:
raise PublishError(f"{repo}:{tag}: HTTP {status} {body[:200]!r}")
return uploaded
def log(msg: str):
print(f"[{time.strftime('%H:%M:%S')}] {msg}", flush=True)
def publish(repo: Path, contour: str, release: str, commit: str, key_file: Path) -> int:
target = f"{SLICE_REGISTRY}/{contour}-iac"
log(f"contour {contour}, source iac@{commit[:8]}, release {release}")
try:
with tempfile.TemporaryDirectory() as tmp:
out = Path(tmp) / "slice.tar"
files, entries = bs.collect(repo, f"clusters/{contour}")
log(f"collected {len(files)} files, entry points: {', '.join(entries)}")
bs.write_tar(repo, files, out)
bs.verify(repo, out, entries)
log(f"verified: kustomize build of {len(entries)} entry point(s) identical to full repository")
layer = out.read_bytes()
new = digest(layer)
log(f"slice size {len(layer)} bytes, digest {new}")
registry = Registry("json_key", key_file.read_text())
current = registry.current_layer(f"{target}")
changed = current != new
log(f"registry {REGISTRY_HOST}/{target}, current " + (current or "absent") + (", content changed" if changed else ", content unchanged"))
tags = [commit[:8], release] + (["current"] if changed else [])
uploaded = registry.push(target, tags, layer, {
"org.opencontainers.image.source": "iac",
"org.opencontainers.image.revision": commit,
"org.opencontainers.image.version": release,
"org.opencontainers.image.created": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
"io.sarex.slice.contour": contour,
"io.sarex.slice.kind": "iac",
})
log(f"layer {'uploaded' if uploaded else 'already in registry'}, tags pushed: {', '.join(tags)}")
except (bs.SliceError, PublishError) as e:
log(f"FAILED: {e}")
return 1
log("done")
return 0
def main() -> int:
ap = argparse.ArgumentParser()
sub = ap.add_subparsers(dest="cmd", required=True)
gen = sub.add_parser("generate")
gen.add_argument("--repo", default=".")
gen.add_argument("--release", required=True)
gen.add_argument("--commit", required=True)
gen.add_argument("--out", required=True)
pub = sub.add_parser("publish")
pub.add_argument("--repo", default=".")
pub.add_argument("--contour", required=True)
pub.add_argument("--release", required=True)
pub.add_argument("--commit", required=True)
pub.add_argument("--key-file", required=True)
args = ap.parse_args()
repo = Path(args.repo).resolve()
if args.cmd == "generate":
contours = list_contours(repo)
Path(args.out).write_text(child_pipeline(contours, args.release, args.commit))
log(f"{len(contours)} contours: {', '.join(contours)}")
return 0
return publish(repo, args.contour, args.release, args.commit, Path(args.key_file))
if __name__ == "__main__":
sys.exit(main())

View File

@ -0,0 +1,93 @@
import importlib.util
import tarfile
from pathlib import Path
import pytest
import yaml
spec = importlib.util.spec_from_file_location("build_slice", Path(__file__).parents[1] / "build-slice.py")
bs = importlib.util.module_from_spec(spec)
spec.loader.exec_module(bs)
def put(root: Path, rel: str, text: str):
p = root / rel
p.parent.mkdir(parents=True, exist_ok=True)
p.write_text(text)
@pytest.fixture
def repo(tmp_path):
put(tmp_path, "clusters/a/kustomization.yaml", "resources:\n - ./flux-system\n - ./layers.yaml\n")
put(tmp_path, "clusters/a/flux-system/kustomization.yaml", "resources:\n - gotk-sync.yaml\n")
put(tmp_path, "clusters/a/flux-system/gotk-sync.yaml",
"apiVersion: kustomize.toolkit.fluxcd.io/v1\nkind: Kustomization\nspec:\n path: ./clusters/a\n")
put(tmp_path, "clusters/a/layers.yaml",
"apiVersion: kustomize.toolkit.fluxcd.io/v1\nkind: Kustomization\nspec:\n path: ./layers/a/00-vault\n")
put(tmp_path, "layers/a/00-vault/kustomization.yaml", "resources:\n - ../../../infrastructure/vault/contour-template\n")
put(tmp_path, "infrastructure/vault/contour-template/kustomization.yaml",
"resources:\n - ../base\npatches:\n - path: vault.yaml\n")
put(tmp_path, "infrastructure/vault/contour-template/vault.yaml", "kind: HelmRelease\n")
put(tmp_path, "infrastructure/vault/base/kustomization.yaml", "resources:\n - helmrelease.yaml\n")
put(tmp_path, "infrastructure/vault/base/helmrelease.yaml", "kind: HelmRelease\n")
put(tmp_path, "infrastructure/vault/base/unused.yaml", "kind: ConfigMap\n")
put(tmp_path, "infrastructure/vault/uralkal/kustomization.yaml", "resources:\n - ../base\n")
put(tmp_path, "clusters/b/kustomization.yaml", "resources:\n - ../../infrastructure/vault/uralkal\n")
return tmp_path
def test_slice_follows_flux_paths_and_skips_foreign(repo):
files, entries = bs.collect(repo, "clusters/a")
names = {str(f) for f in files}
assert "infrastructure/vault/base/helmrelease.yaml" in names
assert "infrastructure/vault/contour-template/vault.yaml" in names
assert "layers/a/00-vault/kustomization.yaml" in names
assert not any("uralkal" in n or n.startswith("clusters/b") for n in names)
assert "infrastructure/vault/base/unused.yaml" not in names
assert entries == ["clusters/a", "layers/a/00-vault"]
def test_remote_reference_stops_build(repo):
put(repo, "clusters/a/kustomization.yaml", "resources:\n - https://github.com/x/y//z\n")
with pytest.raises(bs.SliceError, match="remote reference"):
bs.collect(repo, "clusters/a")
def test_dangling_reference_stops_build(repo):
put(repo, "clusters/a/kustomization.yaml", "resources:\n - ./missing.yaml\n")
with pytest.raises(bs.SliceError, match="does not exist"):
bs.collect(repo, "clusters/a")
def test_tar_is_byte_identical_across_runs(repo, tmp_path_factory):
files, _ = bs.collect(repo, "clusters/a")
out1 = tmp_path_factory.mktemp("o") / "s.tar"
out2 = tmp_path_factory.mktemp("o") / "s.tar"
bs.write_tar(repo, files, out1)
(repo / "clusters/a/layers.yaml").touch()
bs.write_tar(repo, files, out2)
assert out1.read_bytes() == out2.read_bytes()
with tarfile.open(out1) as t:
assert all(m.mtime == 0 for m in t.getmembers())
pspec = importlib.util.spec_from_file_location("publish_slice", Path(__file__).parents[1] / "publish-slice.py")
ps = importlib.util.module_from_spec(pspec)
pspec.loader.exec_module(ps)
def test_every_cluster_with_kustomization_is_a_contour(tmp_path):
put(tmp_path, "clusters/stand/kustomization.yaml", "resources: []\n")
put(tmp_path, "clusters/vad/kustomization.yaml", "resources: []\n")
put(tmp_path, "clusters/contour/apps.yaml", "kind: Kustomization\n")
assert ps.list_contours(tmp_path) == ["stand", "vad"]
def test_child_pipeline_has_one_job_per_contour():
doc = yaml.safe_load(ps.child_pipeline(["stand", "vad"], "3.9.68", "abcdef1234"))
jobs = {k: v for k, v in doc.items() if k != "stages"}
assert sorted(jobs) == ["slice stand", "slice vad"]
assert all(j["allow_failure"] for j in jobs.values())
assert "--contour vad --release 3.9.68 --commit abcdef1234" in jobs["slice vad"]["script"][1]
assert all(ord(ch) < 128 for ch in ps.child_pipeline(["stand"], "1.0.0", "a" * 40))

View File

@ -0,0 +1,14 @@
---
apiVersion: source.toolkit.fluxcd.io/v1
kind: HelmRepository
metadata:
name: yc-oci-charts
namespace: flux-system
spec:
type: oci
interval: 10m0s
url: ${CHART_REPO:=oci://cr.yandex/crp3ccidau046kdj8g9q/charts}
secretRef:
name: ${CHART_REPO_SECRET:=yc-cr-auth}
certSecretRef:
name: chart-repo-ca

View File

@ -0,0 +1,5 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ./helm-repositories.yaml
- ../../../infrastructure/vault/contour-template

View File

@ -0,0 +1,7 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ../../../infrastructure/istio-base/contour-template
- ../../../infrastructure/istio-pilot/contour-template
- ../../../infrastructure/istio-gateway/contour-template
- ../../../infrastructure/cert-manager/contour-template

View File

@ -0,0 +1,6 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ../../../infrastructure/postgresql/contour-template
- ../../../infrastructure/kafka/contour-template
- ../../../infrastructure/rabbitmq/contour-template

View File

@ -0,0 +1,5 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ../../../infrastructure/zitadel/contour-template
- ../../../infrastructure/camunda/contour-template

View File

@ -0,0 +1,4 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ../../../infrastructure/istio-config/contour-template

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,25 @@
apiVersion: source.toolkit.fluxcd.io/v1
kind: GitRepository
metadata:
name: flux-system
namespace: flux-system
spec:
interval: 1m0s
ref:
branch: main
url: http://gitea.gitea.svc:3000/infra/iac.git
secretRef:
name: flux-system
---
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: flux-system
namespace: flux-system
spec:
interval: 10m0s
path: ./clusters/stand
prune: true
sourceRef:
kind: GitRepository
name: flux-system

View File

@ -0,0 +1,13 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- gotk-components.yaml
- gotk-sync.yaml
patches:
- target:
kind: Deployment
name: kustomize-controller
patch: |-
- op: add
path: /spec/template/spec/containers/0/args/-
value: --feature-gates=CancelHealthCheckOnNewRevision=true

View File

@ -0,0 +1,5 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ./flux-system
- ./layers.yaml

View File

@ -0,0 +1,93 @@
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: 00-vault
namespace: flux-system
spec:
interval: 10m0s
timeout: 10m0s
path: ./clusters/stand/00-vault
prune: true
sourceRef:
kind: GitRepository
name: flux-system
postBuild:
substituteFrom:
- kind: ConfigMap
name: contour-vars
---
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: 10-mesh
namespace: flux-system
spec:
interval: 10m0s
timeout: 15m0s
path: ./clusters/stand/10-mesh
prune: true
wait: true
sourceRef:
kind: GitRepository
name: flux-system
postBuild:
substituteFrom:
- kind: ConfigMap
name: contour-vars
---
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: 20-data
namespace: flux-system
spec:
interval: 10m0s
timeout: 20m0s
path: ./clusters/stand/20-data
prune: true
wait: true
sourceRef:
kind: GitRepository
name: flux-system
postBuild:
substituteFrom:
- kind: ConfigMap
name: contour-vars
---
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: 30-platform
namespace: flux-system
spec:
interval: 10m0s
timeout: 35m0s
path: ./clusters/stand/30-platform
prune: true
wait: true
sourceRef:
kind: GitRepository
name: flux-system
postBuild:
substituteFrom:
- kind: ConfigMap
name: contour-vars
---
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: 40-routing
namespace: flux-system
spec:
interval: 10m0s
timeout: 10m0s
path: ./clusters/stand/40-routing
prune: true
wait: true
sourceRef:
kind: GitRepository
name: flux-system
postBuild:
substituteFrom:
- kind: ConfigMap
name: contour-vars

View File

@ -54,4 +54,31 @@ resources:
- ../../apps/pm/uralkal - ../../apps/pm/uralkal
- ../../apps/message-hub/uralkal - ../../apps/message-hub/uralkal
- ../../apps/cde/uralkal - ../../apps/cde/uralkal
- ../../apps/postgres-tls-test/uralkal
patches:
- target:
kind: HelmRelease
patch: |
- op: add
path: /spec/driftDetection
value:
mode: enabled
ignore:
- paths: ["/webhooks"]
target:
kind: MutatingWebhookConfiguration
- paths: ["/webhooks"]
target:
kind: ValidatingWebhookConfiguration
- target:
kind: HelmRelease
name: istiod
patch: |
- op: add
path: /spec/driftDetection/ignore/-
value:
paths: ["/spec/replicas"]
target:
kind: Deployment

View File

@ -12,3 +12,4 @@ resources:
- ../../../infrastructure/kb/vk-infra - ../../../infrastructure/kb/vk-infra
- ../../../infrastructure/gitlab-runner/vk-infra - ../../../infrastructure/gitlab-runner/vk-infra
- ../../../infrastructure/nexus/vk-infra - ../../../infrastructure/nexus/vk-infra
- ../../../infrastructure/outline/vk-infra

View File

@ -0,0 +1,354 @@
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: camunda
namespace: camunda
spec:
dependsOn: []
interval: 5m
timeout: 30m
waitStrategy:
name: legacy
postRenderers:
- kustomize:
patches:
- target:
group: apps
version: v1
kind: Deployment
patch: |-
- op: add
path: /spec/template/spec/nodeSelector
value:
dedicated: processing
- op: add
path: /spec/template/spec/tolerations
value:
- key: dedicated
operator: Equal
value: processing
effect: NoSchedule
- target:
group: apps
version: v1
kind: StatefulSet
patch: |-
- op: add
path: /spec/template/spec/nodeSelector
value:
dedicated: processing
- op: add
path: /spec/template/spec/tolerations
value:
- key: dedicated
operator: Equal
value: processing
effect: NoSchedule
- target:
group: batch
version: v1
kind: Job
patch: |-
- op: add
path: /spec/template/spec/nodeSelector
value:
dedicated: processing
- op: add
path: /spec/template/spec/tolerations
value:
- key: dedicated
operator: Equal
value: processing
effect: NoSchedule
- target:
group: apps
version: v1
kind: Deployment
name: camunda-connectors
patch: |-
- op: add
path: /spec/template/spec/nodeSelector
value:
dedicated: processing
- op: add
path: /spec/template/spec/tolerations
value:
- key: dedicated
operator: Equal
value: processing
effect: NoSchedule
- target:
group: apps
version: v1
kind: Deployment
name: camunda-identity
patch: |-
- op: add
path: /spec/template/spec/nodeSelector
value:
dedicated: processing
- op: add
path: /spec/template/spec/tolerations
value:
- key: dedicated
operator: Equal
value: processing
effect: NoSchedule
- target:
group: apps
version: v1
kind: Deployment
name: camunda-operate
patch: |-
- op: add
path: /spec/template/spec/nodeSelector
value:
dedicated: processing
- op: add
path: /spec/template/spec/tolerations
value:
- key: dedicated
operator: Equal
value: processing
effect: NoSchedule
- target:
group: apps
version: v1
kind: Deployment
name: camunda-optimize
patch: |-
- op: add
path: /spec/template/spec/nodeSelector
value:
dedicated: processing
- op: add
path: /spec/template/spec/tolerations
value:
- key: dedicated
operator: Equal
value: processing
effect: NoSchedule
- target:
group: apps
version: v1
kind: Deployment
name: camunda-tasklist
patch: |-
- op: add
path: /spec/template/spec/nodeSelector
value:
dedicated: processing
- op: add
path: /spec/template/spec/tolerations
value:
- key: dedicated
operator: Equal
value: processing
effect: NoSchedule
- target:
group: apps
version: v1
kind: Deployment
name: camunda-zeebe-gateway
patch: |-
- op: add
path: /spec/template/spec/nodeSelector
value:
dedicated: processing
- op: add
path: /spec/template/spec/tolerations
value:
- key: dedicated
operator: Equal
value: processing
effect: NoSchedule
- target:
group: apps
version: v1
kind: StatefulSet
name: camunda-zeebe
patch: |-
- op: add
path: /spec/template/spec/nodeSelector
value:
dedicated: processing
- op: add
path: /spec/template/spec/tolerations
value:
- key: dedicated
operator: Equal
value: processing
effect: NoSchedule
values:
global:
vault:
enabled: true
role: camunda-contour
authPath: auth/kubernetes
secrets:
postgresql:
path: secrets/data/camunda/identity-db
keys:
password: password
postgresPassword: password
image:
pullSecrets:
- name: regcred
identity:
auth:
publicIssuerUrl: "https://${HOST_CAMUNDA_KEYCLOAK}/auth/realms/camunda-platform"
identity:
redirectUrl: "https://${HOST_CAMUNDA_IDENTITY}"
operate:
redirectUrl: "https://${HOST_CAMUNDA}"
tasklist:
redirectUrl: "https://${HOST_CAMUNDA_TASKLIST}"
optimize:
redirectUrl: "https://${HOST_CAMUNDA_OPTIMIZE}"
identityPostgresql:
enabled: false
auth:
usePasswordFiles: true
primary:
automountServiceAccountToken: true
persistence:
size: 10Gi
storageClass: ${STORAGE_CLASS}
identityKeycloak:
externalDatabase:
host: pg-host.storage.svc.cluster.local
port: 5432
user: bn_keycloak
database: bitnami_keycloak
password: ""
existingSecret: ""
existingSecretPasswordKey: password
postgresql:
enabled: false
auth:
usePasswordFiles: true
primary:
automountServiceAccountToken: true
persistence:
size: 10Gi
storageClass: ${STORAGE_CLASS}
vaultEnv:
enabled: true
role: camunda-contour
authPath: auth/kubernetes
envFiles:
KEYCLOAK_ADMIN_PASSWORD:
path: secrets/data/camunda/keycloak-admin
key: admin-password
KEYCLOAK_PASSWORD:
path: secrets/data/camunda/keycloak-admin
key: admin-password
KEYCLOAK_DATABASE_PASSWORD:
path: secrets/data/camunda/keycloak-db
key: password
global:
storageClass: ${STORAGE_CLASS}
tolerations: []
elasticsearch:
sysctlImage:
registry: cr.yandex
repository: crp3ccidau046kdj8g9q/contour/camunda/os-shell
tag: 12-debian-12-r32
pullSecrets:
- regcred
master:
replicaCount: 1
podAntiAffinityPreset: soft
persistence:
size: 10Gi
storageClass: ${STORAGE_CLASS}
tolerations: []
metrics:
enabled: false
serviceMonitor:
enabled: false
prometheusRule:
enabled: false
tolerations: []
camundaCanary:
enabled: false
prometheusServiceMonitor:
enabled: false
console:
image:
pullSecrets:
- name: regcred
tolerations: []
zeebe:
clusterSize: "1"
partitionCount: "1"
replicationFactor: "1"
pvcStorageClassName: ${STORAGE_CLASS}
image:
pullSecrets:
- name: regcred
tolerations: []
zeebeGateway:
replicas: 1
image:
pullSecrets:
- name: regcred
tolerations: []
identity:
fullURL: "https://${HOST_CAMUNDA_IDENTITY}"
externalDatabase:
enabled: true
host: pg-host.storage.svc.cluster.local
port: 5432
username: identity
database: identity
password: ""
existingSecret: ""
existingSecretPasswordKey: password
image:
pullSecrets:
- name: regcred
tolerations: []
operate:
image:
pullSecrets:
- name: regcred
serviceAccount:
automountServiceAccountToken: true
tolerations: []
tasklist:
image:
pullSecrets:
- name: regcred
serviceAccount:
automountServiceAccountToken: true
tolerations: []
optimize:
image:
pullSecrets:
- name: regcred
serviceAccount:
automountServiceAccountToken: true
tolerations: []
executionIdentity:
image:
pullSecrets:
- name: regcred
tolerations: []
webModeler:
image:
pullSecrets:
- name: regcred
restapi:
tolerations: []
webapp:
tolerations: []
websockets:
tolerations: []
connectors:
image:
pullSecrets:
- name: regcred
serviceAccount:
automountServiceAccountToken: true
tolerations: []

View File

@ -0,0 +1,6 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ../base
patches:
- path: camunda.yaml

View File

@ -0,0 +1,25 @@
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: cert-manager
namespace: cert-manager
spec:
interval: 5m
timeout: 10m
values:
controller:
image:
registry: cr.yandex/crp3ccidau046kdj8g9q/cert-manager
repository: cert-manager
acmesolver:
image:
registry: cr.yandex/crp3ccidau046kdj8g9q/cert-manager
repository: acmesolver
webhook:
image:
registry: cr.yandex/crp3ccidau046kdj8g9q/cert-manager
repository: cert-manager-webhook
cainjector:
image:
registry: cr.yandex/crp3ccidau046kdj8g9q/cert-manager
repository: cainjector

View File

@ -0,0 +1,28 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ../base
patches:
- path: cert-manager.yaml
- target:
group: cert-manager.io
version: v1
kind: ClusterIssuer
name: letsencrypt-issuer
patch: |-
$patch: delete
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-issuer
- target:
group: cert-manager.io
version: v1
kind: ClusterIssuer
name: letsencrypt-issuer-istio
patch: |-
$patch: delete
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-issuer-istio

View File

@ -0,0 +1,4 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ../base

View File

@ -0,0 +1,117 @@
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: istio-config
namespace: default
spec:
dependsOn: []
interval: 5m
timeout: 10m
values:
global:
env: contour
environments:
contour:
istio:
gateways:
platform:
name: platform-gateway
namespace: default
selector:
istio: ingressgateway
servers:
- hosts:
- ${HOST_LOGIN}
- ${HOST_CAMUNDA}
- ${HOST_CAMUNDA_IDENTITY}
- ${HOST_CAMUNDA_KEYCLOAK}
- ${HOST_CAMUNDA_OPTIMIZE}
- ${HOST_CAMUNDA_TASKLIST}
- ${HOST_RABBITMQ}
tls:
credentialName: istio-ingress-tls
virtualServices:
zitadel:
name: zitadel-virt-service
namespace: default
hosts:
- ${HOST_LOGIN}
gateways:
- default/platform-gateway
routes:
- path:
prefix: /
service: zitadel-idp-contour.zitadel.svc.cluster.local
port: 8080
camunda-operate:
name: camunda-operate-virt-service
namespace: default
hosts:
- ${HOST_CAMUNDA}
gateways:
- default/platform-gateway
routes:
- path:
prefix: /
service: camunda-operate.camunda.svc.cluster.local
port: 80
camunda-identity:
name: camunda-identity-virt-service
namespace: default
hosts:
- ${HOST_CAMUNDA_IDENTITY}
gateways:
- default/platform-gateway
routes:
- path:
prefix: /
service: camunda-identity.camunda.svc.cluster.local
port: 80
camunda-keycloak:
name: camunda-keycloak-virt-service
namespace: default
hosts:
- ${HOST_CAMUNDA_KEYCLOAK}
gateways:
- default/platform-gateway
routes:
- path:
prefix: /
service: camunda-keycloak.camunda.svc.cluster.local
port: 80
camunda-optimize:
name: camunda-optimize-virt-service
namespace: default
hosts:
- ${HOST_CAMUNDA_OPTIMIZE}
gateways:
- default/platform-gateway
routes:
- path:
prefix: /
service: camunda-optimize.camunda.svc.cluster.local
port: 80
camunda-tasklist:
name: camunda-tasklist-virt-service
namespace: default
hosts:
- ${HOST_CAMUNDA_TASKLIST}
gateways:
- default/platform-gateway
routes:
- path:
prefix: /
service: camunda-tasklist.camunda.svc.cluster.local
port: 80
rabbitmq:
name: rabbitmq-virt-service
namespace: default
hosts:
- ${HOST_RABBITMQ}
gateways:
- default/platform-gateway
routes:
- path:
prefix: /
service: rabbitmq.rabbitmq.svc.cluster.local
port: 15672

View File

@ -0,0 +1,6 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ../base
patches:
- path: istio-config.yaml

View File

@ -58,6 +58,18 @@ spec:
renewBefore: 360h0m0s renewBefore: 360h0m0s
privateKey: privateKey:
rotationPolicy: Always rotationPolicy: Always
srx-ui-kit-tls:
dnsNames:
- srx-ui-kit.infra.sarex.tech
- srx-ui-kit-stage.infra.sarex.tech
issuerRef:
name: letsencrypt-issuer-istio
kind: ClusterIssuer
spec:
duration: 2160h0m0s
renewBefore: 360h0m0s
privateKey:
rotationPolicy: Always
nexus-tls: nexus-tls:
dnsNames: dnsNames:
- nexus.infra.sarex.tech - nexus.infra.sarex.tech
@ -80,6 +92,28 @@ spec:
renewBefore: 360h0m0s renewBefore: 360h0m0s
privateKey: privateKey:
rotationPolicy: Always rotationPolicy: Always
wiki-tls:
dnsNames:
- wiki.sarex.space
issuerRef:
name: letsencrypt-issuer-istio
kind: ClusterIssuer
spec:
duration: 2160h0m0s
renewBefore: 360h0m0s
privateKey:
rotationPolicy: Always
goalert-tls:
dnsNames:
- goalert.infra.sarex.io
issuerRef:
name: letsencrypt-issuer-istio
kind: ClusterIssuer
spec:
duration: 2160h0m0s
renewBefore: 360h0m0s
privateKey:
rotationPolicy: Always
istio: istio:
gateways: gateways:
vault: vault:
@ -104,6 +138,18 @@ spec:
tls: tls:
credentialName: kb-tls credentialName: kb-tls
mode: SIMPLE mode: SIMPLE
srx-ui-kit:
name: srx-ui-kit-gateway
namespace: istio-system
selector:
istio: ingressgateway
servers:
- hosts:
- srx-ui-kit.infra.sarex.tech
- srx-ui-kit-stage.infra.sarex.tech
tls:
credentialName: srx-ui-kit-tls
mode: SIMPLE
nexus: nexus:
name: nexus-gateway name: nexus-gateway
namespace: istio-system namespace: istio-system
@ -126,6 +172,28 @@ spec:
tls: tls:
credentialName: idp-tls credentialName: idp-tls
mode: SIMPLE mode: SIMPLE
wiki:
name: wiki-gateway
namespace: istio-system
selector:
istio: ingressgateway
servers:
- hosts:
- wiki.sarex.space
tls:
credentialName: wiki-tls
mode: SIMPLE
goalert:
name: goalert-gateway
namespace: istio-system
selector:
istio: ingressgateway
servers:
- hosts:
- goalert.infra.sarex.io
tls:
credentialName: goalert-tls
mode: SIMPLE
virtualServices: virtualServices:
vault-virt-service: vault-virt-service:
namespace: istio-system namespace: istio-system
@ -149,6 +217,28 @@ spec:
prefix: / prefix: /
service: kb-service.kb.svc.cluster.local service: kb-service.kb.svc.cluster.local
port: 80 port: 80
srx-ui-kit-virt-service:
namespace: istio-system
hosts:
- srx-ui-kit.infra.sarex.tech
gateways:
- istio-system/srx-ui-kit-gateway
routes:
- path:
prefix: /
service: universal-static-site-srx-ui-kit-pages.gitlab-pages-srx-ui-kit.svc.cluster.local
port: 80
srx-ui-kit-stage-virt-service:
namespace: istio-system
hosts:
- srx-ui-kit-stage.infra.sarex.tech
gateways:
- istio-system/srx-ui-kit-gateway
routes:
- path:
prefix: /
service: universal-static-site-srx-ui-kit-stage-pages.gitlab-pages-srx-ui-kit-stage.svc.cluster.local
port: 80
nexus-virt-service: nexus-virt-service:
namespace: istio-system namespace: istio-system
hosts: hosts:
@ -171,3 +261,25 @@ spec:
prefix: / prefix: /
service: idp.zitadel.svc.cluster.local service: idp.zitadel.svc.cluster.local
port: 8080 port: 8080
wiki-virt-service:
namespace: istio-system
hosts:
- wiki.sarex.space
gateways:
- istio-system/wiki-gateway
routes:
- path:
prefix: /
service: outline.outline.svc.cluster.local
port: 80
goalert-virt-service:
namespace: istio-system
hosts:
- goalert.infra.sarex.io
gateways:
- istio-system/goalert-gateway
routes:
- path:
prefix: /
service: goalert.goalert.svc.cluster.local
port: 8081

View File

@ -0,0 +1,31 @@
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: ingressgateway
namespace: istio-system
spec:
interval: 5m
timeout: 10m
dependsOn:
- name: istio-base
namespace: istio-system
- name: istiod
namespace: istio-system
values:
_internal_defaults_do_not_set:
replicaCount: ${GATEWAY_REPLICAS}
service:
type: ClusterIP
nodeSelector:
sarex.io/gateway: "true"
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchLabels:
istio: ingressgateway
topologyKey: kubernetes.io/hostname
tolerations:
- key: node-role.kubernetes.io/control-plane
operator: Exists
effect: NoSchedule

View File

@ -0,0 +1,6 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ../base
patches:
- path: istio-gateway.yaml

View File

@ -0,0 +1,12 @@
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: istiod
namespace: istio-system
spec:
interval: 5m
timeout: 10m
values:
global:
imagePullSecrets:
- regcred

View File

@ -0,0 +1,6 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ../base
patches:
- path: istio-pilot.yaml

View File

@ -13,3 +13,34 @@ spec:
global: global:
imagePullSecrets: imagePullSecrets:
- regcred - regcred
# Провайдер журнала доступа для security-audit (access-collector). Сам по себе ничего не включает:
# работает только для workload'ов, на которые ссылается Telemetry (infrastructure/security-audit/mesh).
meshConfig:
extensionProviders:
- name: audit-access
envoyOtelAls:
service: access-collector.security-audit.svc.cluster.local
port: 4317
logFormat:
labels:
sarex_kind: "mesh_access"
start_time: "%START_TIME%"
request_id: "%REQ(X-REQUEST-ID)%"
trace_id: "%TRACE_ID%"
authority: "%REQ(:AUTHORITY)%"
method: "%REQ(:METHOD)%"
path: "%REQ_WITHOUT_QUERY(:PATH)%"
protocol: "%PROTOCOL%"
response_code: "%RESPONSE_CODE%"
response_flags: "%RESPONSE_FLAGS%"
duration_ms: "%DURATION%"
bytes_in: "%BYTES_RECEIVED%"
bytes_out: "%BYTES_SENT%"
x_forwarded_for: "%REQ(X-FORWARDED-FOR)%"
downstream_ip: "%DOWNSTREAM_REMOTE_ADDRESS_WITHOUT_PORT%"
user_sub: "%REQ(X-SAREX-AUDIT-SUB)%"
user_org: "%REQ(X-SAREX-AUDIT-ORG)%"
user_agent: "%REQ(USER-AGENT)%"
route: "%ROUTE_NAME%"
upstream_cluster: "%UPSTREAM_CLUSTER%"
# Authorization, Cookie, Set-Cookie и query-строка не логируются (п.23)

View File

@ -0,0 +1,73 @@
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: kafka
namespace: kafka
spec:
interval: 5m
timeout: 10m
postRenderers:
- kustomize:
patches:
- target:
group: apps
version: v1
kind: StatefulSet
namespace: kafka
patch: |-
- op: add
path: /spec/template/spec/nodeSelector
value:
dedicated: generic
- op: add
path: /spec/template/spec/tolerations
value: []
values:
global:
imagePullSecrets:
- regcred
defaultStorageClass: ${STORAGE_CLASS}
image:
pullSecrets:
- regcred
controller:
replicaCount: 1
automountServiceAccountToken: true
persistence:
size: 8Gi
storageClass: ${STORAGE_CLASS}
overrideConfiguration:
offsets.topic.replication.factor: 1
transaction.state.log.replication.factor: 1
transaction.state.log.min.isr: 1
default.replication.factor: 1
min.insync.replicas: 1
broker:
replicaCount: 0
automountServiceAccountToken: true
listeners:
client:
protocol: SASL_PLAINTEXT
sslClientAuth: "none"
provisioning:
enabled: false
sasl:
managedExistingSecret:
enabled: false
existingSecret: ""
enabledMechanisms: PLAIN,SCRAM-SHA-512
interBrokerMechanism: PLAIN
controllerMechanism: PLAIN
client:
users: []
passwords: ""
tls:
type: PEM
vault:
enabled: true
role: kafka
authPath: auth/kubernetes
secretPath: secrets/data/kafka/bootstrap
clusterIdKey: clusterId
interBrokerPasswordKey: interBrokerPassword
controllerPasswordKey: controllerPassword

View File

@ -0,0 +1,6 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ../base
patches:
- path: kafka.yaml

View File

@ -0,0 +1,113 @@
---
apiVersion: batch/v1
kind: Job
metadata:
name: kafka-scram-users
namespace: kafka
annotations:
kustomize.toolkit.fluxcd.io/force: enabled
spec:
backoffLimit: 2
template:
metadata:
annotations:
sidecar.istio.io/inject: "false"
vault.hashicorp.com/agent-inject: "true"
vault.hashicorp.com/agent-pre-populate-only: "true"
vault.hashicorp.com/auth-path: auth/kubernetes
vault.hashicorp.com/role: kafka
vault.hashicorp.com/agent-inject-secret-admin: secrets/data/kafka/bootstrap
vault.hashicorp.com/agent-inject-template-admin: |
{{- with secret "secrets/data/kafka/bootstrap" -}}{{ index .Data.data "interBrokerPassword" }}{{- end -}}
vault.hashicorp.com/agent-inject-secret-pm-user: secrets/data/kafka/apps/pm
vault.hashicorp.com/agent-inject-template-pm-user: |
{{- with secret "secrets/data/kafka/apps/pm" -}}{{ index .Data.data "username" }}{{- end -}}
vault.hashicorp.com/agent-inject-secret-pm-pass: secrets/data/kafka/apps/pm
vault.hashicorp.com/agent-inject-template-pm-pass: |
{{- with secret "secrets/data/kafka/apps/pm" -}}{{ index .Data.data "password" }}{{- end -}}
vault.hashicorp.com/agent-inject-secret-issues-user: secrets/data/kafka/apps/issues
vault.hashicorp.com/agent-inject-template-issues-user: |
{{- with secret "secrets/data/kafka/apps/issues" -}}{{ index .Data.data "username" }}{{- end -}}
vault.hashicorp.com/agent-inject-secret-issues-pass: secrets/data/kafka/apps/issues
vault.hashicorp.com/agent-inject-template-issues-pass: |
{{- with secret "secrets/data/kafka/apps/issues" -}}{{ index .Data.data "password" }}{{- end -}}
vault.hashicorp.com/agent-inject-secret-flows-user: secrets/data/kafka/apps/flows
vault.hashicorp.com/agent-inject-template-flows-user: |
{{- with secret "secrets/data/kafka/apps/flows" -}}{{ index .Data.data "username" }}{{- end -}}
vault.hashicorp.com/agent-inject-secret-flows-pass: secrets/data/kafka/apps/flows
vault.hashicorp.com/agent-inject-template-flows-pass: |
{{- with secret "secrets/data/kafka/apps/flows" -}}{{ index .Data.data "password" }}{{- end -}}
spec:
serviceAccountName: kafka-kafka-contour
automountServiceAccountToken: true
restartPolicy: Never
imagePullSecrets:
- name: regcred
nodeSelector:
dedicated: generic
securityContext:
runAsUser: 1001
runAsNonRoot: true
fsGroup: 1001
containers:
- name: kafka-scram-users
image: cr.yandex/crp3ccidau046kdj8g9q/contour/kafka-nn/kafka:4.0.0-debian-12-r10
imagePullPolicy: IfNotPresent
env:
- name: KAFKA_HEAP_OPTS
value: -Xmx256m
command:
- /bin/bash
- -ec
- |
K=/opt/bitnami/kafka/bin
ADMIN=kafka-kafka-contour-controller-0.kafka-kafka-contour-controller-headless.kafka.svc.cluster.local:9094
CLIENT=kafka-kafka-contour.kafka.svc.cluster.local:9092
S=/vault/secrets
rc=0
printf 'security.protocol=SASL_PLAINTEXT\nsasl.mechanism=PLAIN\nsasl.jaas.config=org.apache.kafka.common.security.plain.PlainLoginModule required username="inter_broker_user" password="%s";\n' "$(cat $S/admin)" > /tmp/admin.conf
for app in pm issues flows; do
u=$(cat $S/$app-user); p=$(cat $S/$app-pass)
if [ -z "$u" ] || [ -z "$p" ]; then
echo "$app: empty username or password in kafka/apps/$app"; rc=1; continue
fi
if [ -f "/tmp/pw-$u" ]; then
if [ "$(cat /tmp/pw-$u)" != "$p" ]; then
echo "$app: user $u already set from another app with a different password, skipped"; rc=1
else
echo "$app: user $u already set, same password"
fi
continue
fi
printf '%s' "$p" > "/tmp/pw-$u"
$K/kafka-configs.sh --bootstrap-server $ADMIN --command-config /tmp/admin.conf --alter --add-config "SCRAM-SHA-512=[iterations=4096,password=$p]" --entity-type users --entity-name "$u" > /dev/null
echo "$app: SCRAM-SHA-512 set for user $u"
done
sleep 5
for app in pm issues flows; do
u=$(cat $S/$app-user); p=$(cat $S/$app-pass)
printf 'security.protocol=SASL_SSL\nsasl.mechanism=SCRAM-SHA-512\nssl.truststore.type=PEM\nssl.truststore.location=/kafka-ca/ca.crt\nsasl.jaas.config=org.apache.kafka.common.security.scram.ScramLoginModule required username="%s" password="%s";\n' "$u" "$p" > /tmp/$app.conf
if $K/kafka-topics.sh --bootstrap-server $CLIENT --command-config /tmp/$app.conf --list > /dev/null; then
echo "$app: login as $u over SASL_SSL ok"
else
echo "$app: login as $u over SASL_SSL FAILED"; rc=1
fi
done
$K/kafka-configs.sh --bootstrap-server $ADMIN --command-config /tmp/admin.conf --describe --entity-type users
rm -f /tmp/*.conf /tmp/pw-*
exit $rc
resources:
requests:
cpu: 50m
memory: 384Mi
volumeMounts:
- name: kafka-ca
mountPath: /kafka-ca
readOnly: true
volumes:
- name: kafka-ca
secret:
secretName: kafka-kafka-contour-tls
items:
- key: ca.crt
path: ca.crt

View File

@ -2,5 +2,6 @@ apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization kind: Kustomization
resources: resources:
- ../base - ../base
- kafka-scram-users-job.yaml
patches: patches:
- path: kafka.yaml - path: kafka.yaml

View File

@ -0,0 +1,6 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- namespace.yaml
- valkey.yaml
- outline.yaml

View File

@ -0,0 +1,6 @@
apiVersion: v1
kind: Namespace
metadata:
name: outline
labels:
istio-injection: enabled

View File

@ -0,0 +1,139 @@
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: outline
namespace: outline
spec:
interval: 10m
timeout: 10m
chart:
spec:
chart: outline
version: "0.1.0"
sourceRef:
kind: HelmRepository
name: yc-oci-charts
namespace: flux-system
interval: 10m
install:
remediation:
retries: 3
upgrade:
remediation:
retries: 3
dependsOn:
- name: istiod
namespace: istio-system
- name: outline-valkey
namespace: outline
values:
universal-chart:
global:
env: infra
image:
name:
infra: cr.yandex/crp3ccidau046kdj8g9q/outlinewiki/outline:1.12.0
envs:
- name: NODE_ENV
value:
_default: production
- name: URL
value:
_default: https://wiki.sarex.space
- name: PORT
value:
_default: "3000"
- name: WEB_CONCURRENCY
value:
_default: "1"
# TLS терминируется на istio ingressgateway
- name: FORCE_HTTPS
value:
_default: "false"
- name: ENABLE_UPDATES
value:
_default: "false"
- name: LOG_LEVEL
value:
_default: info
- name: PGSSLMODE
value:
_default: disable
- name: FILE_STORAGE
value:
_default: s3
- name: AWS_REGION
value:
_default: ru-msk
- name: AWS_S3_UPLOAD_BUCKET_URL
value:
_default: https://hb.ru-msk.vkcloud-storage.ru
- name: AWS_S3_UPLOAD_BUCKET_NAME
value:
_default: sarex-wiki
- name: AWS_S3_FORCE_PATH_STYLE
value:
_default: "true"
- name: AWS_S3_ACL
value:
_default: private
# VK Object Storage документирует presigned PUT; presigned POST не описан
- name: AWS_S3_UPLOAD_METHOD
value:
_default: put
- name: OIDC_DISPLAY_NAME
value:
_default: Sarex IAM
- name: OIDC_AUTH_URI
value:
_default: https://iam.sarex.io/oauth/v2/authorize
- name: OIDC_TOKEN_URI
value:
_default: https://iam.sarex.io/oauth/v2/token
- name: OIDC_USERINFO_URI
value:
_default: https://iam.sarex.io/oidc/v1/userinfo
- name: OIDC_LOGOUT_URI
value:
_default: https://iam.sarex.io/oidc/v1/end_session
- name: OIDC_USERNAME_CLAIM
value:
_default: preferred_username
- name: OIDC_SCOPES
value:
_default: openid profile email offline_access
# SMTP подключается отдельным шагом: добавить SMTP_HOST, SMTP_PORT,
# SMTP_FROM_EMAIL сюда и SMTP_USERNAME, SMTP_PASSWORD в secretEnvs.
secretEnvs:
- name: SECRET_KEY
secretName:
_default: outline
secretKey: SECRET_KEY
- name: UTILS_SECRET
secretName:
_default: outline
secretKey: UTILS_SECRET
- name: DATABASE_URL
secretName:
_default: outline-postgresql
secretKey: url
- name: REDIS_URL
secretName:
_default: outline
secretKey: REDIS_URL
- name: AWS_ACCESS_KEY_ID
secretName:
_default: outline
secretKey: AWS_ACCESS_KEY_ID
- name: AWS_SECRET_ACCESS_KEY
secretName:
_default: outline
secretKey: AWS_SECRET_ACCESS_KEY
- name: OIDC_CLIENT_ID
secretName:
_default: outline
secretKey: OIDC_CLIENT_ID
- name: OIDC_CLIENT_SECRET
secretName:
_default: outline
secretKey: OIDC_CLIENT_SECRET

View File

@ -0,0 +1,62 @@
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: outline-valkey
namespace: outline
spec:
interval: 10m
timeout: 10m
chart:
spec:
chart: valkey
version: "0.1.0"
sourceRef:
kind: HelmRepository
name: yc-oci-charts
namespace: flux-system
interval: 10m
install:
remediation:
retries: 3
upgrade:
remediation:
retries: 3
values:
fullnameOverride: outline-valkey
image:
registry: cr.yandex
repository: crp3ccidau046kdj8g9q/valkey/valkey
tag: 8.0.2-debian-12-r6
global:
imagePullSecrets:
- istio-registry-secret
security:
allowInsecureImages: true
architecture: standalone
auth:
enabled: true
sentinel: false
existingSecret: outline-valkey
existingSecretPasswordKey: valkey-password
# Outline хранит в Valkey очереди фоновых задач и состояние websocket.
# Персистентность не нужна, вытеснение ключей недопустимо для очередей.
commonConfiguration: |-
appendonly no
save ""
maxmemory 256mb
maxmemory-policy noeviction
primary:
podAnnotations:
sidecar.istio.io/inject: "false"
nodeSelector:
sarex.io/pool: svc
resources:
requests:
cpu: 50m
memory: 128Mi
limits:
memory: 384Mi
persistence:
enabled: false
metrics:
enabled: false

View File

@ -0,0 +1,6 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ../base
patches:
- path: postgresql.yaml

View File

@ -0,0 +1,104 @@
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: postgresql
namespace: postgresql
spec:
interval: 5m
timeout: 2h
install:
timeout: 2h
remediation:
retries: 3
upgrade:
timeout: 2h
remediation:
retries: 3
values:
global:
security:
allowInsecureImages: true
defaultStorageClass: ${STORAGE_CLASS}
postgresql:
auth:
username: ""
database: ""
secretKeys:
userPasswordKey: "postgres-password"
auth:
username: ""
database: ""
secretKeys:
userPasswordKey: "postgres-password"
image:
registry: cr.yandex/crp3ccidau046kdj8g9q
repository: contour/postgresql
tag: 17.0.7
pullPolicy: Always
metrics:
enabled: false
prometheusRule:
enabled: false
primary:
automountServiceAccountToken: true
containerSecurityContext:
readOnlyRootFilesystem: false
persistence:
storageClass: ${STORAGE_CLASS}
size: 20Gi
customLivenessProbe:
exec:
command:
- /bin/sh
- -c
- exec pg_isready -U "postgres" -d postgres -h 127.0.0.1 -p 5432
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 5
successThreshold: 1
failureThreshold: 6
customReadinessProbe:
exec:
command:
- /bin/sh
- -c
- exec pg_isready -U "postgres" -d postgres -h 127.0.0.1 -p 5432
initialDelaySeconds: 5
periodSeconds: 10
timeoutSeconds: 5
successThreshold: 1
failureThreshold: 6
customStartupProbe:
exec:
command:
- /bin/sh
- -c
- exec pg_isready -U "postgres" -d postgres -h 127.0.0.1 -p 5432
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 5
successThreshold: 1
failureThreshold: 6
nodeSelector:
dedicated: generic
tolerations: []
contour:
enabled: true
adminUser: "postgres"
vault:
enabled: true
role: postgresql-zitadel
authPath: auth/kubernetes
secretPath: secrets/data/postgresql-zitadel/admin
secretKey: postgres-password
usersSecretPath: secrets/data/zitadel/postgresql
sharedPreloadLibraries: "timescaledb,pg_stat_statements"
databases:
- name: zitadel
user: zitadel
passwordKey: password
extensions: []
restoreFromDump: false

View File

@ -0,0 +1,6 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ../base
patches:
- path: rabbitmq.yaml

View File

@ -0,0 +1,104 @@
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: rabbitmq
namespace: rabbitmq
spec:
interval: 5m
timeout: 10m
postRenderers:
- kustomize:
patches:
- target:
group: apps
version: v1
kind: StatefulSet
namespace: rabbitmq
patch: |-
- op: add
path: /spec/template/spec/nodeSelector
value:
dedicated: generic
- op: add
path: /spec/template/spec/tolerations
value: []
- target:
group: cert-manager.io
version: v1
kind: Certificate
namespace: gateway
name: rmq-tls
patch: |-
$patch: delete
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: rmq-tls
namespace: gateway
- target:
group: networking.istio.io
version: v1beta1
kind: Gateway
namespace: gateway
name: rmq-gateway
patch: |-
$patch: delete
apiVersion: networking.istio.io/v1beta1
kind: Gateway
metadata:
name: rmq-gateway
namespace: gateway
- target:
group: networking.istio.io
version: v1
kind: VirtualService
namespace: rabbitmq
name: rmq-virt-service
patch: |-
$patch: delete
apiVersion: networking.istio.io/v1
kind: VirtualService
metadata:
name: rmq-virt-service
namespace: rabbitmq
values:
global:
security:
allowInsecureImages: true
virtualService: null
gateway: null
certificate: null
metrics:
serviceMonitor:
enabled: false
default:
enabled: false
perObject:
enabled: false
detailed:
enabled: false
extraServiceMonitors: []
replicaCount: 1
resources:
requests:
memory: 1Gi
persistence:
storageClass: ${STORAGE_CLASS}
size: 10Gi
auth:
securePassword: true
enableLoopbackUser: false
existingPasswordSecret: ""
vault:
enabled: true
role: rabbitmq
authPath: auth/kubernetes
secretPath: secrets/data/rabbitmq/auth
usernameKey: username
passwordKey: password
advancedConfiguration: |-
[
{rabbit, [
{loopback_users, []}
]}
].

View File

@ -0,0 +1,690 @@
# security-audit
Поток ИБ в Kafka, четыре источника:
| Источник | Коллектор | Топик |
|---|---|---|
| журнал аудита kube-apiserver (мастера) | DaemonSet `audit-collector` | `security.k8s-audit` |
| журнал доступа ingress-шлюза Istio | Deployment `access-collector`, pipeline `logs/access` | `security.mesh-access` |
| события Zitadel (Actions v2) | Deployment `access-collector`, pipeline `logs/idp` | `security.idp-events` |
| журнал аудита Vault | sidecar `audit-shipper` в подах Vault → `access-collector`, pipeline `logs/vault` | `security.vault-audit` |
Одно сообщение — одна запись в JSON (`encoding: raw`, без OTLP-обёртки), обогащённая полями `sarex_*`.
Дальнейшие этапы (processor → MinIO/Parquet) — отдельными компонентами.
С observability-коллектором (`infrastructure/opentelemetry-collector`) не связан: отдельный релиз, namespace и конфиг.
| Каталог | Что | Namespace |
|---|---|---|
| `base/` | оба коллектора, NetworkPolicy, AuthorizationPolicy, Namespace | `security-audit` |
| `base/idp-actions-bootstrap/` | Job настройки Target/Execution'ов в Zitadel, скрипт и список событий | `security-audit` |
| `mesh/` | EnvoyFilter и Telemetry шлюза; без `namespace` в kustomization, селектор шлюза — заглушка | `istio-system` |
| `uralkal/` | оверлей: `base` + `mesh`, селектор шлюза, Kafka, `dependsOn` | — |
| `../vault/uralkal-audit/` | Vault-часть журнала аудита Vault: оверлей поверх `vault/uralkal` (не подключён) | `vault` |
| Оверлей | Статус |
|---|---|
| `uralkal` | манифесты готовы, **в `clusters/uralkal` не подключён** |
## Журнал аудита kube-apiserver
DaemonSet `audit-collector-agent` на узлах `node-role.kubernetes.io/control-plane`. Он читает
`/var/log/audit/kube-apiserver-audit*.log` (текущий и ротированные файлы), позиции хранит в `/var/lib/otelcol-audit`
(hostPath), туда же складывает дисковую очередь отправки.
Обработка (`transform/audit`):
- **вырезание шума:** `metadata.managedFields` и аннотация `kubectl.kubernetes.io/last-applied-configuration`
в `requestObject` / `responseObject`;
- **маскирование:** значения параметров `password|passwd|pwd|token|secret|api_key|api-key` в `requestURI`
(`token=abc` → `token=***`), сама команда `exec` не маскируется;
- **классификация** — поля ниже. Правила применяются по порядку, последнее подходящее побеждает.
| Поле | Значение | Условие |
|---|---|---|
| `sarex_contour` | имя контура | env `CONTOUR` из оверлея |
| `sarex_dry_run` | `true` / `false` | `requestURI` содержит `dryRun=All` |
| `sarex_action` | `read` | `verb` ∈ `get`, `list` |
| | `mutate` | `verb` ∈ `create`, `update`, `patch`, `delete`, `deletecollection` |
| | `status` | `objectRef.subresource == status` |
| | `interactive` | `objectRef.subresource` ∈ `exec`, `attach`, `portforward` |
| `sarex_outcome` | `applied` | код 2xx, не dry-run |
| | `dry_run` | код 2xx, dry-run |
| | `denied_authn` | 401 |
| | `denied_authz` | 403 и `authorization.k8s.io/decision=forbid` |
| | `denied_admission` | 4xx при `decision=allow`, кроме 404/409/422 |
| | `invalid` | 422 |
| | `conflict` | 409 |
| | `not_found` | 404 |
| | `error` | 5xx |
| `sarex_actor_type` | `human` | по умолчанию |
| | `serviceaccount` | `system:serviceaccount:*` |
| | `controller` | `system:serviceaccount:kube-system:*`, `system:kube-*`, `system:apiserver` |
| | `node` | `system:node:*` |
| | `gitops` | `system:serviceaccount:flux-system:*` |
`watch` не попадает ни в одну категорию `sarex_action`: поле не выставляется.
Надёжность доставки: `required_acks: -1`, дисковая очередь (`sending_queue.storage`), бесконечные ретраи. При
заполненной очереди срабатывает backpressure на `filelog`, чтение файла приостанавливается. Данные теряются,
только если файл успеет уйти из ротации (10 × 100 МБ) раньше, чем Kafka вернётся.
## Предусловия подключения в контуре
1. **Аудит apiserver включён** (INFRA-3057): флаги `--audit-policy-file`, `--audit-log-path=/var/log/audit/kube-apiserver-audit.log`,
`--audit-log-maxsize=100`, `--audit-log-maxbackup=10`. Каталог `/var/log/audit` есть на всех мастерах.
2. **Kafka:** топик и SCRAM-пользователь.
- Топик `security.k8s-audit`: 1 партиция, `retention.ms=259200000` (3 суток), `max.message.bytes=5242880`,
RF по контуру (в `uralkal` один controller → RF 1).
- Пользователь `security-audit` (SCRAM-SHA-512). ACL не нужны.
Заводится terraform'ом: `infra/terraform`, ветка `contour`, модуль `kafka-topics-incluster`, блок
`environments.<контур>` в `infrastructure.yaml`. Если нужно вручную — из пода контроллера, через внутренний
листенер (`INTERNAL`, 9094, `SASL_PLAINTEXT`/`PLAIN`, `inter_broker_user`):
```bash
kubectl -n kafka exec -it kafka-kafka-contour-controller-0 -c kafka -- bash
```
```bash
cat > /tmp/admin.properties <<EOF
security.protocol=SASL_PLAINTEXT
sasl.mechanism=PLAIN
sasl.jaas.config=org.apache.kafka.common.security.plain.PlainLoginModule required username="inter_broker_user" password="$(cat /vault/secrets/kafka-inter-broker-password)";
EOF
B=localhost:9094
kafka-topics.sh --bootstrap-server $B --command-config /tmp/admin.properties --create \
--topic security.k8s-audit --partitions 1 --replication-factor 1 \
--config retention.ms=259200000 --config max.message.bytes=5242880
kafka-configs.sh --bootstrap-server $B --command-config /tmp/admin.properties --alter \
--entity-type users --entity-name security-audit \
--add-config 'SCRAM-SHA-512=[iterations=4096,password=<пароль>]'
rm /tmp/admin.properties
```
3. **Vault** (`secrets/` — KV v2):
- секрет `secrets/data/security-audit/kafka` с ключами `username`, `password`, `ca_crt`;
`ca_crt` — содержимое `ca.crt` из Secret `kafka/kafka-kafka-contour-tls` (CA генерирует чарт `kafka-contour`,
срок 365 дней; при перевыпуске обновить и здесь);
- политика `read` на `secrets/data/security-audit/kafka`;
- роль `security-audit` в `auth/kubernetes`: bound SA `audit-collector` **и** `access-collector`, namespace
`security-audit`, политика выше.
Заводится в `infra/terraform`, ветка `contour`, модуль `vault-platform-secrets` (роли `vault_kubernetes_auth_backend_role`,
KV-секреты).
4. **Образ** `cr.yandex/crp3ccidau046kdj8g9q/opentelemetry-collector/opentelemetry-collector-contrib:0.121.0-amd64`
доступен в registry контура (тот же, что дефолт чарта `opentelemetry-collector-prod` 0.117.1), `regcred` есть в
namespace `security-audit`.
## Подключение
Отдельным MR после выполнения предусловий — добавить в `clusters/uralkal/kustomization.yaml`:
```yaml
- ../../infrastructure/security-audit/uralkal
```
`dependsOn` в оверлее ждёт HelmRelease `kafka/kafka` и `vault/vault`.
Порядок: сначала должен раскататься провайдер `audit-access` в istiod (`infrastructure/istio-pilot/uralkal/istio-pilot.yaml`,
применяется сразу, как только попадёт в master: кластер его уже подключает). Telemetry из `mesh/` ссылается на этот провайдер по имени. Проверка:
`kubectl -n istio-system get cm istio -o jsonpath='{.data.mesh}' | grep -A2 audit-access`.
## Проверка после подключения
- DaemonSet `security-audit/audit-collector-agent` только на мастерах, все поды `Ready`:
`kubectl -n security-audit get pods -o wide`.
- В логах коллектора нет ошибок: `kubectl -n security-audit logs ds/audit-collector-agent -c opentelemetry-collector-prod`.
- События в топике (из пода контроллера Kafka, с `admin.properties` выше):
`kafka-console-consumer.sh --bootstrap-server localhost:9094 --consumer.config /tmp/admin.properties --topic security.k8s-audit --max-messages 5`.
- `kubectl create secret generic audit-probe --from-literal=k=v` → событие с `objectRef.resource=secrets` уровня
`Metadata`, без `requestObject`/`responseObject`.
- Рестарт пода коллектора (`kubectl delete pod`) → нет дублей и пропусков: сверить `auditID` в топике с файлом журнала
на мастере за интервал рестарта.
## Известное ограничение
Значения env с паролями внутри `requestObject` / `responseObject` workload-ресурсов на уровне `RequestResponse`
коллектор не маскирует: OTTL плохо работает с массивами (`spec.containers[].env[]`). Маскирование будет в processor'е.
До его готовности не повышать уровень для workload-ресурсов выше `Request` в контурах, где пароли могут быть в env.
## Журнал доступа шлюза
Каждый внешний HTTP-запрос через ingress-шлюз (`istio-system`, `istio: ingressgateway`): кто, что, когда, результат,
IP, `x-request-id`, `trace_id`. Поток ИБ отдельно от прикладных журналов (п.20); IP источника для событий приложений
находится по `request_id` (п.15).
```
ingress-шлюз (Envoy)
└─ Telemetry gateway-audit-access ─► провайдер audit-access (envoyOtelAls, meshConfig istiod)
└─ OTLP gRPC, mTLS ─► access-collector.security-audit:4317 (2 реплики, sidecar Istio)
└─ transform/access ─► Kafka security.mesh-access ─► processor (infra/k8s-audit, отдельная задача)
```
### Поля записи
| Поле | Источник Envoy | Примечание |
|---|---|---|
| `sarex_kind` | — | всегда `mesh_access` |
| `start_time` | `%START_TIME%` | |
| `request_id` | `%REQ(X-REQUEST-ID)%` | ключ связи с журналами приложений (п.15) |
| `trace_id` | `%TRACE_ID%` | пусто, пока в mesh выключен трейсинг |
| `authority`, `method`, `protocol` | `:authority`, `:method`, `%PROTOCOL%` | |
| `path` | `%REQ_WITHOUT_QUERY(:PATH)%` | без query; маскирование — ниже |
| `response_code`, `response_flags` | `%RESPONSE_CODE%`, `%RESPONSE_FLAGS%` | `0` — ответа не было |
| `duration_ms`, `bytes_in`, `bytes_out` | `%DURATION%`, `%BYTES_RECEIVED%`, `%BYTES_SENT%` | |
| `x_forwarded_for`, `downstream_ip` | `X-Forwarded-For`, `%DOWNSTREAM_REMOTE_ADDRESS_WITHOUT_PORT%` | за NodePort — см. открытые вопросы |
| `user_sub`, `user_org` | `x-sarex-audit-sub`, `x-sarex-audit-org` | заполняет RequestAuthentication из JWT; **пока её нет — пусто** |
| `user_agent`, `route`, `upstream_cluster` | `User-Agent`, `%ROUTE_NAME%`, `%UPSTREAM_CLUSTER%` | |
| `sarex_contour` | env `CONTOUR` | |
| `sarex_action` | по `method` | `read`: GET/HEAD/OPTIONS; `mutate`: POST/PUT/PATCH/DELETE |
| `sarex_outcome` | по `response_code` | `applied` 2xx–3xx, `denied_authn` 401, `denied_authz` 403, `invalid` прочие 4xx (включая 404), `error` 5xx и `0` |
| `sarex_actor_type` | по `user_sub` | `human`, если есть, иначе `anonymous` |
**Не логируется (п.23):**
- `Authorization`, `Cookie`, `Set-Cookie` — это сами учётные данные: журнал с ними позволяет войти от имени пользователя;
- query-строка — в ней бывают токены публичных ссылок, `token=`, подписи S3 (`X-Amz-Signature`).
Маскирование в пути:
- сегмент после `token|secret|key|signature/` → `***`;
- JWT-подобные строки → `***jwt***`.
Защита от подмены: EnvoyFilter `strip-audit-headers` первым HTTP-фильтром удаляет входящие `x-sarex-audit-sub/org`.
Без него клиент мог бы записать в журнал любого пользователя.
### Пользователь в журнале: RequestAuthentication — не создана (блокер)
`user_sub`/`user_org` должна заполнять `RequestAuthentication` на шлюзе (`outputClaimToHeaders`). Но она отклоняет
**любой** запрос с JWT издателя, которого нет в `jwtRules` (`Jwt issuer is not configured`, 401), на всех хостах шлюза.
А издатели в Уралкалии подтверждаются по репозиторию не все:
| Издатель | Заголовок | `issuer` / JWKS | Статус |
|---|---|---|---|
| Zitadel | `Authorization: Bearer`, `Identity` | `https://login.sarex.local.uralkali.com` (`ExternalDomain` + `ExternalSecure: true` в `infrastructure/zitadel/uralkal`; `ExternalPort` не задан — проверить, что в `/.well-known/openid-configuration` нет `:8080`) | issuer — `TODO(verify)`; префикс заголовка `Identity` не подтверждён |
| django (SimpleJWT, RS512) | `Authorization: Bearer` (режим `sarex`) | в коде `SIMPLE_JWT_ISSUER` по умолчанию `default_issuer` (`apps/django/base/django-configmap.yaml`), в `CONFIGURATION.md` — `django`; в `uralkal` не задан. Ключ — `secrets/data/vault/common/rsa_keys`, в репозитории нет | **не подтверждён** |
| Camunda Keycloak | `Authorization: Bearer` на `camunda-*` | `https://camunda-keycloak.sarex.local.uralkali.com/auth/realms/camunda-platform` (`infrastructure/camunda/uralkal`), JWKS `…/protocol/openid-connect/certs` | подтверждён |
| document-link | `Authorization: Bearer` (зашитый/сервисный JWT) | неизвестен | **не подтверждён** |
Пока таблица не закрыта, журнал пишется без пользователя (`sarex_actor_type=anonymous`). Пользователя для события
можно найти по `request_id` в журналах приложений.
JWKS Zitadel — выбранный вариант, когда дойдёт до включения:
- **inline `jwks` — нет:** Zitadel ротирует ключи подписи по умолчанию раз в часы (`KeyConfig.PrivateKeyLifetime` 6h),
inline-набор устареет за день, и шлюз начнёт отдавать 401 всем.
- **`jwksUri` на внешний адрес, загрузка istiod — только если подтверждено:** istiod резолвит
`login.sarex.local.uralkali.com` и доверяет CA сертификата `uralkal-sarex-tls`. В istiod нет CA заказчика, и
отключать проверку (`JWKS_RESOLVER_INSECURE_SKIP_VERIFY`) нельзя.
- **Рекомендуется:** `PILOT_JWT_ENABLE_REMOTE_JWKS=envoy` (env istiod) и `jwksUri: http://login.sarex.local.uralkali.com:8080/oauth/v2/keys`
плюс `ServiceEntry` хоста `login.sarex.local.uralkali.com:8080` (HTTP, `resolution: DNS`, endpoint
`zitadel-idp-contour.zitadel.svc.cluster.local`). Ключи забирает Envoy шлюза через кластер mesh, без внешнего DNS и CA.
`Host` остаётся внешним доменом инстанса, его Zitadel узнаёт. Прямой `http://zitadel-idp-contour.zitadel.svc:8080/...`
Zitadel отклонит: инстанс определяется по `Host`.
Черновик (не подключён; включать после закрытия таблицы и согласования 401 с владельцами приложений):
```yaml
apiVersion: security.istio.io/v1
kind: RequestAuthentication
metadata: {name: audit-jwt, namespace: istio-system}
spec:
selector: {matchLabels: {istio: ingressgateway}}
jwtRules:
- issuer: https://login.sarex.local.uralkali.com # TODO(verify)
jwksUri: http://login.sarex.local.uralkali.com:8080/oauth/v2/keys
forwardOriginalToken: true
fromHeaders:
- {name: Authorization, prefix: "Bearer "}
- {name: Identity, prefix: "Bearer "} # TODO(verify): префикс
outputClaimToHeaders:
- {header: x-sarex-audit-sub, claim: sub}
- {header: x-sarex-audit-org, claim: "urn:zitadel:iam:user:resourceowner:id"} # TODO(verify): есть ли claim в access token
- issuer: https://camunda-keycloak.sarex.local.uralkali.com/auth/realms/camunda-platform
jwksUri: http://camunda-keycloak.camunda.svc.cluster.local/auth/realms/camunda-platform/protocol/openid-connect/certs # TODO(verify)
forwardOriginalToken: true
# - issuer: <django SIMPLE_JWT_ISSUER> # блокер: issuer и публичный ключ
```
### Предусловия подключения
- Топик `security.mesh-access`: 3 партиции, `retention.ms=259200000`, `max.message.bytes=1048576`, RF по контуру —
тем же путём, что `security.k8s-audit` (terraform или команды выше). Пользователь — тот же `security-audit`, ACL не нужны.
- Роль Vault `security-audit`: в bound SA добавлен `access-collector` (см. предусловия выше).
- Провайдер `audit-access` раскатан в istiod (см. «Подключение»).
### Sidecar и Vault
- Namespace `security-audit` без `istio-injection`: sidecar получают только поды с меткой `sidecar.istio.io/inject=true`
(webhook `object.sidecar-injector.istio.io`). Это `access-collector`. У `audit-collector` метка `"false"`.
Метка `istio-injection: disabled` отключила бы инъекцию совсем, даже по метке пода.
- `PeerAuthentication` в репозитории нет, поэтому режим PERMISSIVE. Шлюз шлёт в коллектор по auto-mTLS (оба в mesh),
коллектор в Kafka — тоже через mTLS (`kafka` в mesh), внутри остаётся `SASL_SSL`.
- Vault Agent (`agent-init-first`, pre-populate) + istio-proxy: по паттерну контура (`apps/bi`, `apps/bim`, `apps/cde`…)
`traffic.sidecar.istio.io/excludeOutboundPorts: "8200"`. Init-агент ходит в Vault мимо iptables-перехвата,
даже если `istio-init` отработал раньше.
- `holdApplicationUntilProxyStarts`: kafka exporter подключается к брокерам на старте и без готового proxy падает.
- `istio-init` требует `NET_ADMIN`/`NET_RAW`: enforce `privileged` это разрешает, профиль `warn: baseline` даст
предупреждения при создании подов — ожидаемо.
### Проверка после подключения
- Провайдер и порядок фильтров на шлюзе:
```bash
POD=$(kubectl -n istio-system get pod -l istio=ingressgateway -o jsonpath='{.items[0].metadata.name}')
istioctl -n istio-system proxy-config cluster $POD | grep access-collector
istioctl -n istio-system proxy-config listener $POD -o json \
| jq -r '.[].filterChains[].filters[] | select(.name=="envoy.filters.network.http_connection_manager")
| .typedConfig.httpFilters[].name' | uniq
```
Первым в списке — `security-audit.strip-audit-headers`, раньше `envoy.filters.http.jwt_authn` (когда появится).
В выводе `listener` есть `envoy.access_loggers.open_telemetry`.
- `curl -H 'x-sarex-audit-sub: spoof' https://sarex.local.uralkali.com/...` → в Kafka `user_sub` пустой.
- Запрос с `?token=canary` → строки `canary` нет в топике.
- После включения RequestAuthentication:
- запрос с валидным токеном → `user_sub` заполнен;
- с невалидным токеном Zitadel → 401 (так работает RequestAuthentication). **Согласовать с владельцами приложений до включения.**
- `kubectl -n security-audit scale deploy/access-collector --replicas=0` → трафик шлюза не падает (ALS асинхронный).
Потери видны в статистике Envoy: `kubectl -n istio-system exec $POD -- pilot-agent request GET stats | grep access_logs`.
Вернуть 2 реплики.
- Чтение топика: как для `security.k8s-audit`, `--topic security.mesh-access`.
### Мониторинг
- `otelcol_receiver_accepted_log_records{receiver="otlp"}` у `access-collector`: алерт, если 0 за 10 минут при живом
трафике шлюза (`istio_requests_total` на `ingressgateway`).
- `otelcol_exporter_sent_log_records` / `otelcol_exporter_send_failed_log_records` (`exporter="kafka/access"`),
`otelcol_exporter_queue_size` против `otelcol_exporter_queue_capacity`, `otelcol_exporter_enqueue_failed_log_records`.
- Метрики коллектора отдаются на `:8888` пода. Prometheus в Уралкалии по репозиторию не найден — сбор метрик отдельно.
### Открытые вопросы
- **IP клиента (п.15).** Шлюз — одна реплика на control-plane за Service `NodePort` (30080/30443) без
`externalTrafficPolicy`, т. е. `Cluster`. kube-proxy делает SNAT, и в `downstream_ip` попадает IP узла, а не клиента.
Нужно выяснить, есть ли перед NodePort балансировщик заказчика и передаёт ли он `X-Forwarded-For`. Если да —
`meshConfig.defaultConfig.gatewayTopology.numTrustedProxies` (или аннотация `proxy.istio.io/config` на шлюзе) по числу
прокси, иначе Envoy не доверяет XFF. Если балансировщика нет — `externalTrafficPolicy: Local` и DNS/LB только на
узел шлюза. Ничего из этого не меняли.
- **Статика** (`*/static/*`, фронтенды) пишется в журнал. Исключать ли — решение ИБ (фильтр в `mesh/telemetry.yaml`).
- **Запросы между сервисами** (sidecar'ы приложений) — отдельный Telemetry с фильтром, не в этой задаче.
- **Очередь в памяти:** `queue_size: 50000` считается в запросах (батчах до 2000 записей), а не в записях. При долгой
недоступности Kafka упрётся в `memory_limiter`/лимит 512Mi раньше, чем в `queue_size`. Записи сверх этого теряются,
шлюз не блокируется.
## События Zitadel
Вход, неуспешный вход, блокировки, изменения пользователей, ролей и members (п.14–16 документа ИБ).
```
Zitadel (Actions v2: Execution с условием event → Target restAsync, JSON по HTTP)
└─ http://access-collector.security-audit.svc.cluster.local:8088/zitadel/events (mTLS, sidecar у обоих)
└─ webhookevent/zitadel ─► transform/idp ─► Kafka security.idp-events ─► processor (infra/k8s-audit, idp/)
```
Target и Execution'ы заводит Job `idp-actions-bootstrap` (`base/idp-actions-bootstrap/`) через API Zitadel.
Список событий — `files/events.txt`.
### Почему без HMAC
Zitadel подписывает запросы (`X-ZITADEL-Signature`, HMAC-SHA256 тела), но `webhookevent` подпись проверять не умеет.
Вместо подписи источник подтверждается на транспортном уровне:
- **mTLS Istio:** у Zitadel и коллектора есть sidecar, identity источника — SA `zitadel`;
- **AuthorizationPolicy `access-collector`:** на 8088 только `POST /zitadel/events` от `cluster.local/ns/zitadel/sa/zitadel`,
на 4317 только из `istio-system`, остальное запрещено (ALLOW-политика закрывает всё, что не разрешено);
- **NetworkPolicy:** 8088 только из namespace `zitadel` от подов `app.kubernetes.io/name=idp-contour,component=start`.
**Условие:** Zitadel в mesh. Сейчас это так: ns `zitadel` с `istio-injection: enabled`, у Deployment метка
`sidecar.istio.io/inject: "true"` (чарт `idp-contour` 4.12.13). Если Zitadel выведут из mesh, у его запросов не будет
principal, и коллектор начнёт отвечать 403. Тогда либо вернуть sidecar, либо перейти на приёмник с проверкой HMAC
(signingKey из `CreateTarget` — во Vault).
Probe'ы kubelet AuthorizationPolicy не блокирует: при `rewriteAppHTTPProbers` (по умолчанию) sidecar переписывает
HTTP-probe на pilot-agent `:15020`, а тот ходит в приложение напрямую, мимо Envoy и RBAC-фильтра. При подключении сбора
метрик коллектора (`:8888`) нужно отдельное правило в политике.
### Поля события
Тело запроса Zitadel — `ContextInfoEvent` (`internal/repository/execution/queue.go`, v4.12.2), все поля `omitempty`.
| Поле | Откуда | Примечание |
|---|---|---|
| `event_type` | Zitadel | например, `user.human.password.check.failed` |
| `created_at` | Zitadel | RFC3339Nano |
| `aggregateID`, `aggregateType` | Zitadel | объект события (пользователь, грант, организация…) |
| `resourceOwner`, `instanceID` | Zitadel | организация и инстанс |
| `userID` | Zitadel | кто выполнил действие; нет — системное событие |
| `sequence`, `version` | Zitadel | |
| `details.user_name` | `event_payload.userName` | только `user.human.added`, `user.human.selfregistered` |
| `details.role_keys`, `details.project_id` | `event_payload.roleKeys`, `.projectId` | только `user.grant.*` |
| `sarex_kind` | — | `idp_event` |
| `sarex_contour` | env `CONTOUR` | |
| `sarex_outcome` | по `event_type` | `denied_authn` для `*.check.failed`, иначе `applied` |
| `sarex_actor_type` | по `userID` | `human`, без `userID` — `system` |
**`event_payload` удаляется целиком (п.23).** В payload бывают хеш пароля (`user.human.password.changed`),
секреты OTP, client secret, ПДн. Allowlist безопаснее denylist'а: новое поле в новой версии Zitadel не уйдёт в Kafka само.
Нужные ИБ поля копируются в `details` явно.
- `TODO(verify)`: allowlist стартовый. Уточнить по реальным событиям на стенде, какие ещё поля нужны processor'у.
Тело, которое не разобралось как JSON, заменяется маркером `{"sarex_kind": "idp_event", "sarex_error": "unparsed"}`:
сырой текст мог бы унести payload. В 4.12.2 `event_payload` — объект (`json.RawMessage`); вариант с base64-строкой тоже
поддержан.
### Job `idp-actions-bootstrap`
- **Образ.** В registry контура нет образа с `curl` и `jq` вместе. init-контейнер из `contour/zitadel/kubectl:1.33.4`
(уже нужен setup-Job'у Zitadel) копирует `jq` 1.6 с библиотеками в emptyDir. Скрипт идёт в
`contour/kafka-nn/kafka:4.0.0-debian-12-r10` (`curl` 7.88). Оба на Debian 12. `TODO(verify)`: заменить на один
toolbox-образ.
- **Инстанс Zitadel** выбирается заголовком `x-zitadel-instance-host` (+ `x-zitadel-public-host`, `X-Forwarded-Proto: https`).
Это `InstanceHostHeaders`/`PublicHostHeaders` из `defaults.yaml` v4.12.2. `Host` остаётся именем Service: при подмене
`Host` на внешний домен istio-proxy не нашёл бы маршрут и отправил бы запрос в passthrough, без mTLS.
- **Mesh.** Job в mesh (`sidecar.istio.io/inject=true`, `holdApplicationUntilProxyStarts`). В конце скрипт вызывает
`POST 127.0.0.1:15020/quitquitquit`, istio-proxy завершается, под — `Completed`. STRICT mTLS у Zitadel сейчас нет, но
Job к нему готов.
- **Vault:** роль `security-audit-bootstrap`, `secrets/data/security-audit/zitadel-bootstrap` → `pat` →
`/vault/secrets/zitadel-pat`; `excludeOutboundPorts: 8200` — паттерн контура.
- **Скрипт** (`files/idp-actions-bootstrap.sh`, POSIX sh) идемпотентен:
1. ждёт `/debug/ready` (10 попыток × 15 с);
2. печатает все типы событий инстанса (`known: …`, `POST /admin/v1/events/types/_search`);
3. ищет Target `security-audit-idp-events` по имени и создаёт или обновляет его (`restAsync`, endpoint, `timeout: 10s`);
4. для каждого события — `PUT /v2/actions/executions` с условием `{"event": {"event": "<тип>"}}`. Чужие targets
Execution'а сохраняются, свой добавляется в конец;
5. неизвестное имя события — ошибка: остальные события обрабатываются, код выхода ≠ 0.
PAT не печатается (`set -x` нет, заголовки — в файле в tmpfs), signingKey из ответов не выводится.
- **Перезапуск.** ConfigMap'ы скрипта и событий — из `configMapGenerator` (хеш в имени). Правка любого файла меняет
spec Job'а, и Flux пересоздаёт его (`kustomize.toolkit.fluxcd.io/force: enabled`). После `ttlSecondsAfterFinished` (сутки)
Job удаляется, и Flux создаёт его заново: раз в сутки идёт сверка, повторный запуск ничего не меняет.
- **Порядок.** У Job нет `dependsOn`: он стартует вместе с HelmRelease'ами. Если Zitadel ещё не готов — до 10 попыток,
дальше `backoffLimit: 3`. Execution'ы ссылаются на Target по id, коллектор может подняться позже: до его готовности
события теряются (повторов нет, см. риски). Подключать модуль, когда Zitadel уже работает.
### Предусловия подключения
- Топик `security.idp-events`: 3 партиции, `retention.ms=259200000`, RF по контуру, тем же путём, что остальные
топики. Пользователь `security-audit`, ACL не нужны.
- Сервисный пользователь Zitadel `security-audit-bootstrap` (machine user) с ролью `IAM_OWNER` на инстансе.
Нужны `action.target.read/write/delete`, `action.execution.read/write`, `events.read`. Его PAT — во Vault
`secrets/data/security-audit/zitadel-bootstrap`, ключ `pat`.
- Роль Vault `security-audit-bootstrap` в `auth/kubernetes`: bound SA `idp-actions-bootstrap`, namespace `security-audit`,
политика `read` на `secrets/data/security-audit/zitadel-bootstrap`.
- Feature flag не нужен: в v4.12.2 `actions` в `/v2/features/instance` удалён (reserved), Actions v2 включены всегда.
Проверка API изнутри кластера (ожидается `"https://login.sarex.local.uralkali.com"`):
```bash
kubectl -n zitadel exec deploy/zitadel-idp-contour -c istio-proxy -- \
curl -s -H 'x-zitadel-instance-host: login.sarex.local.uralkali.com' -H 'X-Forwarded-Proto: https' \
http://zitadel-idp-contour.zitadel.svc.cluster.local:8080/.well-known/openid-configuration
```
### Проверка после подключения
- `kubectl -n security-audit logs job/idp-actions-bootstrap -c bootstrap`: `Target …: создан`, по событиям
`создано`/`без изменений`, `ошибок=0`. Строки `known:` — сверка имён с инстансом.
- Создать тестового пользователя → в `security.idp-events` есть `user.human.added` без `event_payload` и с
`details.user_name`.
- Неверный пароль → `user.human.password.check.failed`, `sarex_outcome=denied_authn`.
- Канарейка в пароле тестового пользователя → её нет в топике (`grep` по выводу `kafka-console-consumer.sh`).
- `curl -X POST http://access-collector.security-audit:8088/zitadel/events -d '{}'` из пода в другом namespace → таймаут
(NetworkPolicy). Из пода с sidecar в `zitadel`, но не от SA `zitadel` → `403 RBAC: access denied`.
### Риски
- **Повторов доставки нет.** Для `restAsync` Zitadel 4.12.2 только пишет ошибку в лог (`internal/execution`): коллектор
или Kafka недоступны → событие потеряно. Что смягчает:
- 2 реплики, PDB, очередь отправки;
- 200 приходит после приёма в `batch`, дальше доставка в Kafka с ретраями.
Алерты:
- `otelcol_exporter_send_failed_log_records{exporter="kafka/idp"}`;
- нет `otelcol_receiver_accepted_log_records{receiver="webhookevent/zitadel"}` в рабочее время.
- **Полный источник — Events API Zitadel** (`POST /admin/v1/events/_search`). Суточную сверку количества по типам можно
добавить позже.
- **Размер тела.** `webhookevent` 0.121 читает тело одним `bufio.Scanner` без явного буфера: событие больше ~64 КиБ,
вероятно, отбрасывается. Ответ при этом 200. `TODO(verify)` на стенде; события пользователей обычно в разы меньше.
- В 0.121 у `webhookevent` нет `header_attribute_regex`, есть только `required_header`: заголовки в запись не попадают.
### Откат
Снять Target со всех Execution'ов из списка и удалить его — скрипт с `--remove`:
```bash
kubectl -n security-audit get job idp-actions-bootstrap -o json \
| jq 'del(.metadata.uid, .metadata.resourceVersion, .metadata.creationTimestamp, .metadata.labels,
.metadata.annotations, .metadata.ownerReferences, .spec.selector, .status)
| .spec.template.metadata.labels |= with_entries(select(.key | test("controller-uid|job-name") | not))
| .metadata.name = "idp-actions-remove" | .spec.template.spec.containers[0].args = ["--remove"]' \
| kubectl create -f -
```
Пока модуль подключён, Flux вернёт Execution'ы при следующем пересоздании Job'а. Для постоянного отключения убрать
`idp-actions-bootstrap` из `base/kustomization.yaml` или модуль из кластера.
## Журнал аудита Vault
П.19 документа ИБ: аудит Vault включён, чувствительные значения хешируются HMAC-SHA256, журнал идёт в поток ИБ.
П.23: секретов в журнале нет.
```
Vault ──audit device "file"──► /vault/audit/audit.log (VCT "audit", свой PVC у каждой реплики)
│ sidecar audit-rotate (busybox): ротация по размеру + SIGHUP vault (shareProcessNamespace)
│ sidecar audit-shipper (otelcol-contrib): filelog, чекпоинты и очередь — /vault/audit/.otelcol
▼ OTLP gRPC, mTLS (Vault и коллектор в mesh)
access-collector: otlp/vault :4319 → logs/vault → Kafka security.vault-audit
```
Vault-часть — оверлей `infrastructure/vault/uralkal-audit/` (наследует `vault/uralkal`), **не подключён**.
### Почему файл и sidecar
Vault работает **fail-closed**: если включённый audit device не может записать событие, Vault перестаёт обслуживать
запросы. Поэтому в пути записи нет сети, только локальный файл. Kafka, коллектор и сеть могут лежать сколько угодно:
Vault пишет в файл, shipper догоняет потом. Остаётся один риск того же класса — **полный диск**: ротация и очередь
ограничены, на заполнение PVC нужен алерт.
Что хешируется: file device по умолчанию заменяет все строковые значения запросов и ответов (секреты, токены,
`client_token`, accessor'ы) на `hmac-sha256:…` с ключом, известным только Vault. Одинаковые значения дают одинаковый
хеш: по нему можно сверять события (`vault write sys/audit-hash/file input=…`), не раскрывая значения.
**`log_raw` не включается никогда**: с ним в журнал попадают секреты открытым текстом, а журнал уходит в Kafka и S3.
Кредов Kafka у sidecar'а нет и быть не может: Vault Agent в поде самого Vault невозможен, Vault стартует запечатанным.
Креды есть только у `access-collector`.
### Решения с последствиями
- **Том: VCT, а не отдельный PVC.** В Уралкалии 3 реплики HA/raft (дефолт чарта, `ha.enabled: true`, `replicas: 3`;
`vault/uralkal` это не меняет). Каждой реплике нужен свой том. Это `server.auditStorage` чарта →
`volumeClaimTemplates: audit` (5Gi `local-path`, `TODO(verify)` объём). `volumeClaimTemplates` — **неизменяемое поле**
StatefulSet: обычный `helm upgrade` упадёт, нужна процедура с `kubectl delete sts --cascade=orphan` (ниже).
- **Unseal вручную.** `vault-unseal` в Уралкалии не подключён (есть только в `vk-infra`, `yc-infra-prod`). Ключи — в Secret
`vault/vault-secret` (`docs/closed-contour-deployment.md`). Новый pod template перезапустит все 3 пода, каждый
стартует sealed. Readiness чарта — `vault status`: sealed-под не Ready, и `RollingUpdate` ждёт его unseal, прежде чем
перейти к следующему. Поды распечатываются по одному, кворум raft (2 из 3) сохраняется.
- **mTLS без PERMISSIVE.** У подов Vault есть istio-proxy: ns `vault` с `istio-injection: enabled`, у чарта нет opt-out.
`PeerAuthentication` в контуре нет, т. е. PERMISSIVE по умолчанию. shipper шлёт plaintext gRPC (`tls.insecure: true`) до
своего istio-proxy, дальше auto-mTLS. На коллекторе AuthorizationPolicy: 4319 только от
`cluster.local/ns/vault/sa/vault-vault-contour`, плюс NetworkPolicy только из ns `vault`, `app.kubernetes.io/name=vault-contour`,
`component=server`. `TODO(verify)`: `kubectl -n vault get pod vault-vault-contour-0 -o jsonpath='{.spec.containers[*].name}'`
содержит `istio-proxy`. Если Vault окажется вне mesh, principal не будет, коллектор ответит 403, shipper накопит
очередь, а потом перестанет читать файл. Vault продолжит работать, но после KEEP ротаций начнутся потери.
Тогда — вариант (б): TLS на `otlp/vault` (cert-manager), CA в sidecar. Вариант (а) (PERMISSIVE на 4319) ничего не даёт,
пока нет STRICT, а principal без mTLS всё равно пуст.
- **Общий PID namespace и UID vault у sidecar'ов.** SIGHUP можно отправить только процессу своего UID: CAP_KILL у
non-root контейнера не работает, ambient capabilities нет. Файл `0600` читается только UID 100. Последствие:
sidecar'ы видят процесс vault и могут читать `/proc/<pid>/environ`, а при разрешающем `kernel.yama.ptrace_scope` и
dumpable-процессе — и его память. Смягчение: образы только из registry контура (busybox закреплён digest'ом),
`readOnlyRootFilesystem`, `drop: [ALL]`, `no-new-privileges`, без shell-доступа в под. `TODO(verify)`: значение
`ptrace_scope` на узлах `dedicated: generic`.
- **Отклонения от исходного шаблона:**
- `rotate.sh` проверяет pid vault **до** `mv`. Без SIGHUP Vault продолжил бы писать в `audit.log.1`, новый файл
не появился бы, ротация встала бы, а файл рос бы до заполнения диска;
- `queue_size` shipper'а — 1000, а не 20000: 20000 батчей по 500 записей не помещаются в PVC. При полной очереди
filelog перестаёт читать, и буфером служат сами файлы (KEEP × MAX_BYTES);
- `compaction` у `file_storage`: иначе файл bbolt-очереди не уменьшается.
- **ConfigMap'ы без хеша в имени** (`audit-shipper-config`, `audit-rotate-script`): правка не перезапускает Vault сама
(рестарт = unseal). Применить — ручной рестарт подов по одному с unseal.
### Включение audit device
Через terraform контура (`infra/terraform`, ветка `contour`, рядом с `vault-platform-secrets`) — в этой задаче не сделано:
```hcl
resource "vault_audit" "file" {
type = "file"
path = "file"
options = {
file_path = "/vault/audit/audit.log"
mode = "0600"
format = "json"
elide_list_responses = "true" # Vault 1.21.2 (образ vault:1.21.2) — поддерживается
}
}
```
Альтернатива — CLI на активном узле: `vault audit enable file file_path=/vault/audit/audit.log mode=0600 format=json elide_list_responses=true`.
Audit device общий для кластера raft: **все** реплики должны иметь `/vault/audit` до включения. Иначе узел без тома,
став активным, не сможет писать журнал и перестанет обслуживать запросы.
### Порядок подключения (строго)
1. Топик `security.vault-audit`: 3 партиции, `retention.ms=259200000`, RF по контуру. Пользователь `security-audit`,
ACL не нужны (как у остальных топиков модуля).
2. `security-audit` подключён и работает: `access-collector` с `logs/vault`, порт 4319 в Service.
3. Подготовка Vault:
- `vault operator raft list-peers`: 3 voter'а, лидер есть;
- `vault operator raft snapshot save /tmp/pre-audit.snap`, снапшот скопировать из пода;
- ключи unseal из `vault/vault-secret` под рукой.
4. Переключение оверлея. VCT неизменяем, поэтому HelmRelease приостанавливается до пересоздания StatefulSet:
```bash
flux suspend helmrelease vault -n vault
```
MR: в `clusters/uralkal/kustomization.yaml` `../../infrastructure/vault/uralkal` → `../../infrastructure/vault/uralkal-audit`,
дождаться применения Kustomization `flux-system`: ConfigMap'ы появились, HelmRelease обновлён, но приостановлен.
```bash
kubectl -n vault get cm audit-shipper-config audit-rotate-script
kubectl -n vault delete sts vault-vault-contour --cascade=orphan # поды и PVC data остаются, Vault работает
kubectl -n vault get pods -l component=server # все 3 Running, unsealed
flux resume helmrelease vault -n vault # helm создаёт STS с новым template и VCT audit
```
5. RollingUpdate пересоздаёт поды с конца (`-2`, `-1`, `-0`). Каждый новый под стартует sealed:
`kubectl -n vault exec vault-vault-contour-N -c vault -- vault operator unseal` (порог ключей), дождаться Ready,
и только потом следующий под.
6. После раскатки:
- 3 пода Ready, в каждом контейнеры `vault`, `audit-shipper`, `audit-rotate`, `istio-proxy`;
- PVC `audit-vault-vault-contour-{0,1,2}` в состоянии Bound;
- `vault operator raft list-peers` — 3 voter'а;
- на каждом поде `kubectl -n vault exec vault-vault-contour-N -c vault -- sh -c 'touch /vault/audit/.w && rm /vault/audit/.w'`.
7. Только теперь — `vault_audit` (terraform) или `vault audit enable`. Если включить раньше, enable упадёт: Vault
проверяет запись при включении.
8. Проверка (ниже).
### ⚠ Откат
**Сначала** `vault audit disable file` (или убрать `vault_audit` из terraform и применить), убедиться, что
`vault audit list` пуст. **Потом** вернуть `vault/uralkal`. Удаление VCT — тоже изменение неизменяемого поля, поэтому
та же процедура: suspend → `delete sts --cascade=orphan` → resume → unseal по одному. PVC `audit-*` после отката
остаются, удалить вручную, когда журнал уже не нужен.
Если вернуть оверлей с включённым audit device, у Vault пропадёт `/vault/audit`, он не сможет писать журнал и
перестанет обслуживать запросы.
### Проверка после подключения
- `vault audit list`: device `file/`.
- Чтение тестового секрета → событие в `security.vault-audit` с `sarex_kind=vault_audit`, значение —
`hmac-sha256:…`, записи `"type": "request"` нет.
- Запрос без прав → `sarex_outcome=denied_authz`; неудачный логин → `denied_authn`.
- `kubectl -n security-audit scale deploy/access-collector --replicas=0` на 5 минут → Vault отвечает. После возврата
2 реплик события дочитаны без пропусков (сверка `request.id`).
- Искусственная ротация: `MAX_BYTES` поменять в ConfigMap нельзя, это env в values. Временно на одном поде:
`kubectl -n vault exec vault-vault-contour-N -c audit-rotate -- sh -c 'MAX_BYTES=1 INTERVAL=1 timeout 5 sh /scripts/rotate.sh'`
→ появился `audit.log.1`, новый `audit.log` создан Vault, события не потеряны.
### Мониторинг и алерты
- **Заполнение PVC `audit-*` > 80% — критично:** при полном диске Vault встаёт. Метрика `kubelet_volume_stats_*`.
- Ошибки и отставание `audit-shipper`:
- `otelcol_exporter_send_failed_log_records{exporter="otlp/audit"}`;
- `otelcol_exporter_queue_size` против `otelcol_exporter_queue_capacity`.
Метрики на `:8888` пода Vault; сбора метрик в Уралкалии пока нет.
- Отключение audit device: событие с путём `sys/audit/*` в самом журнале → правило алерта в processor.
- Строки `rotate: ОШИБКА` в логе `audit-rotate`.
### Открытые вопросы
- Размер PVC, `MAX_BYTES`, `KEEP`, `queue_size` — по замеру объёма. `KEEP × MAX_BYTES` должен перекрывать худший простой
коллектора, а их сумма с очередью — оставаться ниже 80% PVC.
- Фильтрация шумных путей средствами Vault: в OSS есть только `elide_list_responses` и `hmac_accessor`; фильтры audit
(`filter`) — Enterprise. Поэтому `request`-записи отбрасываются в shipper'е.
- Какой узел что пишет: standby в OSS пересылает запросы на активный, поэтому основной поток — с лидера; shipper есть на
всех трёх.
## Проверить перед подключением
Сверено по репозиторию и чартам 01.11.2026, доступа к кластеру не было.
| Что | Результат | Как проверено |
|---|---|---|
| Образ и `command.name` | `.../opentelemetry-collector/opentelemetry-collector-contrib:0.121.0-amd64`, `otelcol-contrib` | дефолт `values.yaml` чарта `opentelemetry-collector-prod` 0.117.1 (appVersion 0.120.0, тег образа 0.121.0); `otelcol-contrib components` в образе: filelog, transform, kafka, file_storage, health_check, memory_limiter, batch есть |
| Синтаксис kafka exporter | для 0.121.0 корректен (`topic`/`encoding` наверху, `auth.tls`) | `otelcol-contrib validate` в том же образе — OK |
| Kafka Service и порт | `kafka-kafka-contour.kafka.svc.cluster.local:9092`, `SASL_SSL` (**не** `kafka.kafka...`) | `helm template` чарта `kafka-contour` 0.1.15 с values `uralkal`; тот же адрес у `apps/*/uralkal` |
| CA Kafka | Secret `kafka/kafka-kafka-contour-tls`, ключ `ca.crt` (helm `genCA`, 365 дней) | шаблон `tls-secret.yaml` чарта; `apps/issues/uralkal` монтирует его же |
| Kafka 4.0.0 и `protocol_version: 2.8.0` | совместимо (Kafka 4 требует ≥ 2.1) | `appVersion` чарта `kafka-contour` |
| Vault | HelmRelease `vault/vault`; injector ходит на `http://vault-vault-contour.vault.svc:8200` | `infrastructure/vault/uralkal`, `helm template` `vault-contour` 0.2.3 |
| Узлы мастеров | метка и taint `node-role.kubernetes.io/control-plane` | так же сажается `istio-gateway/uralkal` |
| Рендер чарта | нет `hostPort`, Service, ClusterRole; nodeSelector/tolerations/securityContext как заданы | `helm template` 0.117.1 с values из собранного HelmRelease |
| Логика `transform` | 5 тестовых событий классифицированы верно, `token=***`, `managedFields` вырезаны | запуск коллектора локально в том же образе с `file`-экспортёром вместо kafka |
| **Журнал доступа шлюза** (02.11.2026) | | |
| Шлюз | HelmRelease `istio-system/ingressgateway`, метка `istio: ingressgateway`, 1 реплика на control-plane, `NodePort` 30080/30443, `externalTrafficPolicy` не задан (`Cluster`) | `helm template` `istio-gateway-contour` 1.24.0 с values `uralkal` |
| istiod | `istiod-contour` 1.24.0, ревизий нет; кроме `extensionProviders` mesh config не изменился | `helm template` до и после правки, diff только в ConfigMap `istio` |
| Инъекция sidecar | ns `istio-injection=enabled` или ns без метки + метка пода `sidecar.istio.io/inject=true`; `disabled` блокирует оба | селекторы MutatingWebhookConfiguration из рендера `istiod-contour` |
| mTLS | `PeerAuthentication` в репозитории нет → PERMISSIVE | поиск по `infrastructure/`, `clusters/uralkal`, чарту `istio-config-contour` |
| Vault + sidecar | `excludeOutboundPorts: "8200"` рядом с `agent-init-first` | `apps/bi`, `apps/bim`, `apps/cde/base` |
| Образ access-collector | тот же; `otlp` receiver, `transform`, `kafka` exporter есть | `otelcol-contrib validate` — OK |
| Логика `transform/access` | 7 записей в формате Envoy ALS: `-` → пусто, `token/***`, `***jwt***`, классы верные | локальный коллектор, OTLP/HTTP → `file`-экспортёр |
| Схемы Istio | EnvoyFilter (`v1alpha3`) и Telemetry (`v1`) проходят CRD-схемы `istio-base-contour` 1.24.0 | свой проверщик по `openAPIV3Schema`; `istioctl`/`kubeconform` на машине нет. Конфиг Lua (`patch.value`) схемой не проверяется |
| **События Zitadel** (02.11.2026) | | |
| Zitadel в mesh | ns `zitadel` `istio-injection: enabled`, у Deployment `sidecar.istio.io/inject: "true"`, SA `zitadel`, метки `app.kubernetes.io/name=idp-contour`, `component=start`; trust domain `cluster.local` | `helm template` `idp-contour` 4.12.13 с values `uralkal`, mesh config istiod |
| Версия Zitadel | образ `contour/zitadel/zitadel:4.12.2` | `values.yaml` чарта |
| Actions v2 API | `POST /v2/actions/targets`, `POST /v2/actions/targets/{id}` (update), `POST …/targets/search` (`targetNameFilter`), `PUT /v2/actions/executions` (`targets` — список id), `POST …/executions/search` (`inConditionsFilter`); условие `event.event`/`event.group`/`event.all`; feature flag не нужен | proto `zitadel/action/v2`, `feature/v2` тега v4.12.2 |
| Формат события | `ContextInfoEvent`, `event_payload` — `json.RawMessage`; повторов для async нет | `internal/repository/execution/queue.go`, `internal/execution/*.go` v4.12.2 |
| Имена событий | все 58 из `events.txt` есть в `internal/repository/{user,usergrant,org,project,instance,member}` v4.12.2; агрегата `group` нет | исходники; окончательно — строки `known:` в логе Job'а |
| `webhookevent` 0.121 | есть в образе; 200 после `ConsumeLogs` (с `batch` — после приёма в батч), 500 при ошибке; одна запись на запрос; `required_header` есть, `header_attribute_regex` нет | исходники receiver v0.121.0; локальный прогон |
| OTTL | `ParseJSON`, `Decode(…, "base64")`, `IsString`, `IsMatch`, `IsMap`, литерал map, `set` во вложенный отсутствующий map — работают | `validate` и прогон 6 событий: канарейки (хеш, base64, не-JSON) не дошли |
| Образ Job | `curl` + `jq` вместе нет; `kubectl:1.33.4` (jq 1.6) + `kafka:4.0.0-debian-12-r10` (curl 7.88) | `docker run` образов из registry контура |
| **Журнал аудита Vault** (02.11.2026) | | |
| Чарт `vault-contour` 0.2.3 | values: `server.auditStorage` (VCT `audit` + mount в vault), `extraContainers`, `volumes`, `volumeMounts`, `shareProcessNamespace`, `extraEnvironmentVars` — postRenderers не нужны | шаблоны `server-statefulset.yaml`, `_helpers.tpl` |
| Режим Vault | HA raft, 3 реплики, VCT `data`, `RollingUpdate`, `podManagementPolicy: Parallel`; UID/GID 100/1000, `fsGroup: 1000`; SA `vault-vault-contour`; метки `app.kubernetes.io/name=vault-contour`, `component=server` | `helm template` с values `vault/uralkal` |
| Unseal | ручной: `vault-unseal` в Уралкалии не подключён; readiness `vault status` держит rollout до unseal | `clusters/*`, `docs/closed-contour-deployment.md`, `values.yaml` чарта |
| Istio у Vault | ns `vault` `istio-injection: enabled`, opt-out в чарте нет → в mesh; PeerAuthentication нет | рендер, `infrastructure/vault/base/namespace.yaml` |
| Версия Vault | 1.21.2: `elide_list_responses` есть, file device по SIGHUP переоткрывает файл | тег образа |
| Образы | `opentelemetry-collector-contrib:0.121.0-amd64`; busybox 1.37.0 `contour/local-path-provisioner-nn/busybox@sha256:11f8…` (sh, stat, pidof, kill, mv) | `docker run` из registry |
| `vault/uralkal-audit` | собирается; postRenderers (nodeSelector/tolerations) на месте; ConfigMap'ы в `vault`; `vault/uralkal` побайтно тот же | `kubectl kustomize`, diff |
| Итоговый StatefulSet | `shareProcessNamespace: true`, VCT `data` + `audit`, оба sidecar'а UID 100/GID 1000, `/vault/audit` rw у vault и sidecar'ов | `helm template` + post-render тем же kustomize-патчем, что у Flux |
| Ротация и доставка | фиктивный `vault` (по SIGHUP переоткрывает файл) + `rotate.sh` (`MAX_BYTES=20000`, 16 ротаций) + shipper + access-collector: 1500/1500 событий, 0 дублей, `request` отброшены, `denied_authz` размечен; то же при остановке коллектора на 25 с | Docker: общий PID namespace, том с правами 100:1000/0600, `--read-only`, `--cap-drop ALL` |
| `rotate.sh` | `sh -n` busybox/dash/bash --posix — OK; без процесса vault ротация пропускается (файл не переименован); SIGTERM — выход за 0 с | busybox из registry. shellcheck на машине нет |
| Скрипт | `sh -n` dash/bash --posix/busybox — OK; mock: создание, повтор без изменений, объединение с чужим target, обновление endpoint, `--remove`, API недоступен, 401; утечек PAT/signingKey нет | mock Zitadel на Python, запуск в образе kafka с `--read-only`, uid 1001. shellcheck на машине нет |
Остаётся открытым:
- `TODO(verify)` **nodelocaldns** (`169.254.25.10`) в NetworkPolicy: в репозитории не подтверждается. При дефолте kubespray
включён, если нет — правило безвредно.
- **RequestAuthentication** не создана: не подтверждены издатели django и document-link, `issuer` Zitadel (`TODO(verify)`) —
см. «Пользователь в журнале». Пока `user_sub` пустой.
- **IP клиента за NodePort** — см. «Открытые вопросы» журнала доступа.
- `set(body, attributes)` + `encoding: raw` даёт JSON-объект (проверено локально через `file`-экспортёр, raw-маршалер
сериализует map в JSON) — подтвердить на стенде чтением топика. Там же проверить, что Envoy шлёт labels строками
(сравнения `response_code == "401"`).
- `TODO(verify)` **API Zitadel изнутри кластера** с `x-zitadel-instance-host` (команда в «События Zitadel»): по
исходникам должно работать, запрос не выполнялся.
- `TODO(verify)` **allowlist полей** `details` по реальным событиям; **лимит ~64 КиБ** тела у `webhookevent`.
- `TODO(verify)` **toolbox-образ** для Job вместо пары kubectl + kafka.
- `TODO(verify)` **Vault:** istio-proxy у подов Vault; `ptrace_scope` на узлах; объём PVC `audit`, `MAX_BYTES`/`KEEP`/`queue_size`
по замеру; создание STS заново через `helm upgrade` после `--cascade=orphan` (Helm создаёт отсутствующий ресурс — проверить
на стенде до Уралкалия).
- Некритичные предупреждения 0.121.0: `service::telemetry::metrics::address` устарел (так же во всех коллекторах репо);
OTTL-пути без префикса `log.` (контекст выводится автоматически).

View File

@ -0,0 +1,34 @@
# Транспортная аутентификация вместо HMAC-подписи Zitadel: mTLS Istio + проверка identity источника.
# Как только к workload применяется хотя бы одна ALLOW-политика, весь остальной трафик к нему запрещён.
# - Probe'ы kubelet не блокируются: sidecar переписывает их на pilot-agent (:15020), а тот обращается
# к приложению напрямую, мимо Envoy и его RBAC-фильтра.
# - При подключении сбора метрик коллектора (:8888) добавить отдельное правило для источника сбора.
# Условие: Zitadel в mesh (ns zitadel istio-injection=enabled, под с sidecar.istio.io/inject=true), иначе
# у его запросов нет principal и правило 1 их отклонит.
apiVersion: security.istio.io/v1
kind: AuthorizationPolicy
metadata:
name: access-collector
spec:
selector:
matchLabels:
security-audit/collector: access # podLabels HelmRelease access-collector
action: ALLOW
rules:
- from:
- source:
principals: ["cluster.local/ns/zitadel/sa/zitadel"] # SA Deployment zitadel-idp-contour
to:
- operation: {ports: ["8088"], methods: ["POST"], paths: ["/zitadel/events"]}
- from:
- source:
namespaces: ["istio-system"]
to:
- operation: {ports: ["4317"]}
# Журнал аудита Vault: поды StatefulSet vault-vault-contour в mesh (ns vault, istio-injection=enabled).
# Если Vault выведут из mesh — principal пропадёт, будет 403, shipper копит очередь на PVC (см. README).
- from:
- source:
principals: ["cluster.local/ns/vault/sa/vault-vault-contour"]
to:
- operation: {ports: ["4319"]}

View File

@ -0,0 +1,270 @@
# Журнал доступа ingress-шлюза Istio: envoyOtelAls (провайдер audit-access в meshConfig istiod) → OTLP gRPC → Kafka.
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: access-collector
namespace: security-audit
spec:
interval: 10m
timeout: 10m
chart:
spec:
chart: opentelemetry-collector-prod
version: "0.117.1"
sourceRef:
kind: HelmRepository
name: yc-oci-charts
namespace: flux-system
interval: 10m
install:
remediation:
retries: 3
upgrade:
remediation:
retries: 3
values:
mode: deployment
replicaCount: 2
fullnameOverride: access-collector
# Тот же образ, что у audit-collector: otlp receiver, transform, kafka exporter есть в сборке.
image:
repository: cr.yandex/crp3ccidau046kdj8g9q/opentelemetry-collector/opentelemetry-collector-contrib
tag: "0.121.0-amd64"
command:
name: otelcol-contrib
imagePullSecrets:
- name: regcred
podDisruptionBudget:
enabled: true
minAvailable: 1
# Мягкая: при одном доступном узле вторая реплика всё равно встанет.
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
topologyKey: kubernetes.io/hostname
labelSelector:
matchLabels:
security-audit/collector: access
clusterRole:
create: false
serviceAccount:
create: true
name: access-collector # bound SA роли Vault security-audit (вместе с audit-collector)
presets:
logsCollection: {enabled: false}
hostMetrics: {enabled: false}
kubernetesAttributes: {enabled: false}
kubeletMetrics: {enabled: false}
kubernetesEvents: {enabled: false}
clusterMetrics: {enabled: false}
ports:
otlp:
enabled: true
containerPort: 4317
servicePort: 4317
protocol: TCP
appProtocol: grpc # Istio выбирает протокол по appProtocol
# События Zitadel Actions v2 (Target restAsync) — обычный JSON по HTTP, приёмник webhookevent.
zitadel-events:
enabled: true
containerPort: 8088
servicePort: 8088
protocol: TCP
appProtocol: http
# Журнал аудита Vault от sidecar'а audit-shipper в поде Vault (OTLP gRPC).
otlp-vault:
enabled: true
containerPort: 4319
servicePort: 4319
protocol: TCP
appProtocol: grpc
otlp-http: {enabled: false}
jaeger-compact: {enabled: false}
jaeger-thrift: {enabled: false}
jaeger-grpc: {enabled: false}
zipkin: {enabled: false}
metrics: {enabled: false}
service:
enabled: true
podSecurityContext:
runAsNonRoot: true
runAsUser: 10001
runAsGroup: 10001
seccompProfile: {type: RuntimeDefault}
securityContext:
runAsNonRoot: true
runAsUser: 10001
readOnlyRootFilesystem: true
allowPrivilegeEscalation: false
privileged: false
capabilities:
drop: [ALL]
resources:
requests: {cpu: 100m, memory: 128Mi}
limits: {cpu: 1000m, memory: 512Mi}
podLabels:
security-audit/collector: access # селектор NetworkPolicy и anti-affinity
sidecar.istio.io/inject: "true" # mTLS внутри mesh (п.30); namespace без istio-injection
podAnnotations:
sidecar.istio.io/inject: "true"
# Kafka exporter подключается к брокерам на старте: без готового istio-proxy коллектор упадёт.
proxy.istio.io/config: '{"holdApplicationUntilProxyStarts": true}'
# Паттерн контура для Vault + sidecar (apps/*/base): трафик агента к Vault мимо istio-proxy.
traffic.sidecar.istio.io/excludeOutboundPorts: "8200"
vault.hashicorp.com/agent-inject: "true"
vault.hashicorp.com/agent-init-first: "true"
vault.hashicorp.com/agent-pre-populate-only: "true"
vault.hashicorp.com/agent-requests-cpu: "25m"
vault.hashicorp.com/agent-limits-cpu: "100m"
vault.hashicorp.com/auth-path: auth/kubernetes
vault.hashicorp.com/role: security-audit
# Многострочные шаблоны — см. helmrelease-collector.yaml (toYaml | tpl в чарте).
vault.hashicorp.com/agent-inject-secret-kafka-username: secrets/data/security-audit/kafka
vault.hashicorp.com/agent-inject-template-kafka-username: |-
{{ "{{- with secret \"secrets/data/security-audit/kafka\" -}}" }}
{{ "{{ .Data.data.username }}" }}
{{ "{{- end -}}" }}
vault.hashicorp.com/agent-inject-secret-kafka-password: secrets/data/security-audit/kafka
vault.hashicorp.com/agent-inject-template-kafka-password: |-
{{ "{{- with secret \"secrets/data/security-audit/kafka\" -}}" }}
{{ "{{ .Data.data.password }}" }}
{{ "{{- end -}}" }}
vault.hashicorp.com/agent-inject-secret-kafka-ca.crt: secrets/data/security-audit/kafka
vault.hashicorp.com/agent-inject-template-kafka-ca.crt: |-
{{ "{{- with secret \"secrets/data/security-audit/kafka\" -}}" }}
{{ "{{ .Data.data.ca_crt }}" }}
{{ "{{- end -}}" }}
# Контур-специфичное (CONTOUR, KAFKA_BROKERS) задаётся в оверлее через extraEnvs.
alternateConfig:
extensions:
health_check: {endpoint: "${env:MY_POD_IP}:13133"}
receivers:
otlp:
protocols:
grpc:
endpoint: "${env:MY_POD_IP}:4317"
max_recv_msg_size_mib: 16
# Подпись HMAC (X-ZITADEL-Signature) не проверяется: доступ ограничен mTLS + AuthorizationPolicy
# (только SA zitadel) + NetworkPolicy. Отвечает 200 после ConsumeLogs, т. е. после приёма в batch.
# Отдельный порт: доступ на 4319 только для SA Vault (AuthorizationPolicy), 4317 — только шлюз.
otlp/vault:
protocols:
grpc:
endpoint: "${env:MY_POD_IP}:4319"
webhookevent/zitadel:
endpoint: "${env:MY_POD_IP}:8088"
path: /zitadel/events
health_path: /zitadel/health
processors:
memory_limiter: {check_interval: 1s, limit_mib: 400, spike_limit_mib: 100}
transform/access:
error_mode: ignore
log_statements:
- context: log
statements:
# labels из envoyOtelAls приходят атрибутами — делаем из них тело (в Kafka уходит body как JSON)
- set(body, attributes)
- set(body["sarex_contour"], "${env:CONTOUR}")
# Envoy пишет "-" для пустых значений
- set(body["user_sub"], "") where body["user_sub"] == "-"
- set(body["user_org"], "") where body["user_org"] == "-"
- set(body["trace_id"], "") where body["trace_id"] == "-"
# маскирование: токены в самом пути (публичные ссылки и т. п.)
- replace_pattern(body["path"], "(?i)(token|secret|key|signature)/[^/]+", "$$1/***")
- replace_pattern(body["path"], "[A-Za-z0-9_-]{20,}\\.[A-Za-z0-9_-]{20,}\\.[A-Za-z0-9_-]{10,}", "***jwt***")
# классификация
- set(body["sarex_action"], "read") where body["method"] == "GET" or body["method"] == "HEAD" or body["method"] == "OPTIONS"
- set(body["sarex_action"], "mutate") where body["method"] == "POST" or body["method"] == "PUT" or body["method"] == "PATCH" or body["method"] == "DELETE"
- set(body["sarex_outcome"], "applied") where Int(body["response_code"]) >= 200 and Int(body["response_code"]) < 400
- set(body["sarex_outcome"], "denied_authn") where body["response_code"] == "401"
- set(body["sarex_outcome"], "denied_authz") where body["response_code"] == "403"
- set(body["sarex_outcome"], "invalid") where Int(body["response_code"]) >= 400 and Int(body["response_code"]) < 500 and body["response_code"] != "401" and body["response_code"] != "403"
- set(body["sarex_outcome"], "error") where Int(body["response_code"]) >= 500 or body["response_code"] == "0"
- set(body["sarex_actor_type"], "anonymous")
- set(body["sarex_actor_type"], "human") where body["user_sub"] != ""
transform/idp:
error_mode: ignore
log_statements:
- context: log
statements:
- set(body, ParseJSON(body))
# Не JSON — тело целиком заменяется маркером: сырой текст мог бы унести event_payload в Kafka (п.23).
- 'set(body, {"sarex_kind": "idp_event", "sarex_error": "unparsed"}) where not IsMap(body)'
# В 4.12.2 event_payload — json.RawMessage (объект); base64-строку тоже поддерживаем
- set(body["event_payload"], ParseJSON(Decode(body["event_payload"], "base64"))) where IsString(body["event_payload"])
# allowlist (п.23): нужные ИБ поля копируются явно, payload удаляется ЦЕЛИКОМ
# (в событиях бывают хеши паролей, OTP-секреты, client secret, ПДн)
# TODO(verify): список полей по типам событий уточнить по реальным событиям на стенде
- set(body["details"]["user_name"], body["event_payload"]["userName"]) where IsMatch(body["event_type"], "^user\\.human\\.(added|selfregistered)$")
- set(body["details"]["role_keys"], body["event_payload"]["roleKeys"]) where IsMatch(body["event_type"], "^user\\.grant\\.")
- set(body["details"]["project_id"], body["event_payload"]["projectId"]) where IsMatch(body["event_type"], "^user\\.grant\\.")
- delete_key(body, "event_payload")
# обогащение и классификация
- set(body["sarex_kind"], "idp_event")
- set(body["sarex_contour"], "${env:CONTOUR}")
- set(body["sarex_outcome"], "applied")
- set(body["sarex_outcome"], "denied_authn") where IsMatch(body["event_type"], "check\\.failed$")
- set(body["sarex_actor_type"], "human")
- set(body["sarex_actor_type"], "system") where body["userID"] == "" or body["userID"] == nil
batch: {send_batch_size: 1000, send_batch_max_size: 2000, timeout: 2s}
exporters:
# Общие поля Kafka — YAML-якоря (&kafka_*), kafka/idp ссылается на них (*kafka_*).
kafka/access:
brokers: &kafka_brokers ["${env:KAFKA_BROKERS}"]
protocol_version: &kafka_protocol_version 2.8.0
topic: security.mesh-access
encoding: raw # в сообщение — только body (JSON записи), без OTLP-обёртки
auth: &kafka_auth
sasl: {mechanism: SCRAM-SHA-512, username: "${file:/vault/secrets/kafka-username}", password: "${file:/vault/secrets/kafka-password}"}
tls: {insecure: false, ca_file: /vault/secrets/kafka-ca.crt}
producer: &kafka_producer {required_acks: -1, compression: zstd, max_message_bytes: 1048576}
# Очередь в памяти (диска у Deployment нет): при падении пода неотправленные записи теряются.
sending_queue: &kafka_sending_queue {enabled: true, queue_size: 50000, num_consumers: 4}
retry_on_failure: &kafka_retry {enabled: true, initial_interval: 5s, max_interval: 60s, max_elapsed_time: 0}
kafka/idp:
brokers: *kafka_brokers
protocol_version: *kafka_protocol_version
topic: security.idp-events
encoding: raw
auth: *kafka_auth
producer: *kafka_producer
sending_queue: *kafka_sending_queue # своя очередь у каждого экспортёра, общие только настройки
retry_on_failure: *kafka_retry
# Body уже JSON-объект (json_parser в audit-shipper, sarex_* там же) — transform не нужен.
kafka/vault:
brokers: *kafka_brokers
protocol_version: *kafka_protocol_version
topic: security.vault-audit
encoding: raw
auth: *kafka_auth
producer: *kafka_producer
sending_queue: *kafka_sending_queue
retry_on_failure: *kafka_retry
service:
extensions: [health_check]
pipelines:
logs/access:
receivers: [otlp]
processors: [memory_limiter, transform/access, batch]
exporters: [kafka/access]
logs/idp:
receivers: [webhookevent/zitadel]
processors: [memory_limiter, transform/idp, batch]
exporters: [kafka/idp]
logs/vault:
receivers: [otlp/vault]
processors: [memory_limiter, batch]
exporters: [kafka/vault]
telemetry:
logs: {level: info}
metrics: {address: "${env:MY_POD_IP}:8888"}

View File

@ -0,0 +1,245 @@
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: audit-collector
namespace: security-audit
spec:
interval: 10m
timeout: 10m
chart:
spec:
chart: opentelemetry-collector-prod
version: "0.117.1"
sourceRef:
kind: HelmRepository
name: yc-oci-charts
namespace: flux-system
interval: 10m
install:
remediation:
retries: 3
upgrade:
remediation:
retries: 3
values:
mode: daemonset
fullnameOverride: audit-collector
# Дефолт чарта 0.117.1 (appVersion 0.120.0), задан явно: contrib-сборка с filelog, transform,
# kafka exporter, file_storage, health_check, memory_limiter. Зеркало в registry контура — см. README.
image:
repository: cr.yandex/crp3ccidau046kdj8g9q/opentelemetry-collector/opentelemetry-collector-contrib
tag: "0.121.0-amd64"
command:
name: otelcol-contrib
imagePullSecrets:
- name: regcred
# Только мастера: журнал аудита есть только там.
nodeSelector:
node-role.kubernetes.io/control-plane: ""
tolerations:
- key: node-role.kubernetes.io/control-plane
operator: Exists
effect: NoSchedule
rollout:
rollingUpdate:
maxUnavailable: 1
# Доступ к Kubernetes API коллектору не нужен.
clusterRole:
create: false
serviceAccount:
create: true
name: audit-collector # = роль Vault security-audit, bound SA
presets:
logsCollection: {enabled: false}
hostMetrics: {enabled: false}
kubernetesAttributes: {enabled: false}
kubeletMetrics: {enabled: false}
kubernetesEvents: {enabled: false}
clusterMetrics: {enabled: false}
# В daemonset-режиме чарт вешает hostPort на OTLP/Jaeger/Zipkin: приёмники не нужны,
# а hostPort конфликтует с другими DaemonSet-коллекторами на тех же узлах.
ports:
otlp: {enabled: false}
otlp-http: {enabled: false}
jaeger-compact: {enabled: false}
jaeger-thrift: {enabled: false}
jaeger-grpc: {enabled: false}
zipkin: {enabled: false}
metrics: {enabled: false}
service:
enabled: false
# Журнал аудита — 0600 root:root. Root нужен как владелец файла, capabilities не нужны.
podSecurityContext:
runAsUser: 0
runAsGroup: 0
seccompProfile: {type: RuntimeDefault}
securityContext:
runAsUser: 0
runAsNonRoot: false
readOnlyRootFilesystem: true
allowPrivilegeEscalation: false
privileged: false
capabilities:
drop: [ALL]
resources:
requests: {cpu: 50m, memory: 128Mi}
limits: {cpu: 500m, memory: 512Mi}
extraVolumes:
- name: audit-log
hostPath: {path: /var/log/audit, type: DirectoryOrCreate}
- name: otelcol-storage
hostPath: {path: /var/lib/otelcol-audit, type: DirectoryOrCreate}
extraVolumeMounts:
- {name: audit-log, mountPath: /var/log/audit, readOnly: true}
- {name: otelcol-storage, mountPath: /var/lib/otelcol-audit}
# Метка исключает под из webhook'а инъекции Istio ещё до мутации (namespace без istio-injection).
podLabels:
sidecar.istio.io/inject: "false"
podAnnotations:
sidecar.istio.io/inject: "false"
vault.hashicorp.com/agent-inject: "true"
vault.hashicorp.com/agent-init-first: "true"
vault.hashicorp.com/agent-pre-populate-only: "true"
vault.hashicorp.com/agent-requests-cpu: "25m"
vault.hashicorp.com/agent-limits-cpu: "100m"
vault.hashicorp.com/auth-path: auth/kubernetes
vault.hashicorp.com/role: security-audit
# Чарт рендерит podAnnotations через toYaml | tpl: однострочный шаблон toYaml переносит,
# и литерал {{ "..." }} рвётся. Поэтому по строке на действие (block scalar не переносится).
vault.hashicorp.com/agent-inject-secret-kafka-username: secrets/data/security-audit/kafka
vault.hashicorp.com/agent-inject-template-kafka-username: |-
{{ "{{- with secret \"secrets/data/security-audit/kafka\" -}}" }}
{{ "{{ .Data.data.username }}" }}
{{ "{{- end -}}" }}
vault.hashicorp.com/agent-inject-secret-kafka-password: secrets/data/security-audit/kafka
vault.hashicorp.com/agent-inject-template-kafka-password: |-
{{ "{{- with secret \"secrets/data/security-audit/kafka\" -}}" }}
{{ "{{ .Data.data.password }}" }}
{{ "{{- end -}}" }}
vault.hashicorp.com/agent-inject-secret-kafka-ca.crt: secrets/data/security-audit/kafka
vault.hashicorp.com/agent-inject-template-kafka-ca.crt: |-
{{ "{{- with secret \"secrets/data/security-audit/kafka\" -}}" }}
{{ "{{ .Data.data.ca_crt }}" }}
{{ "{{- end -}}" }}
# Контур-специфичное (CONTOUR, KAFKA_BROKERS) задаётся в оверлее через extraEnvs.
# Полный конфиг вместо мержа с дефолтами чарта.
alternateConfig:
extensions:
health_check:
endpoint: ${env:MY_POD_IP}:13133
file_storage/audit:
directory: /var/lib/otelcol-audit
compaction:
on_start: true
directory: /var/lib/otelcol-audit
receivers:
filelog/audit:
include:
- /var/log/audit/kube-apiserver-audit*.log # текущий и ротированные (lumberjack)
start_at: beginning
storage: file_storage/audit # позиции чтения переживают рестарт
max_log_size: 8MiB # дефолт 1MiB режет RequestResponse
poll_interval: 1s
include_file_name: false
operators:
- type: json_parser
parse_to: body
processors:
memory_limiter:
check_interval: 1s
limit_mib: 400
spike_limit_mib: 100
transform/audit:
error_mode: ignore
log_statements:
- context: log
statements:
# --- объём и шум ---
- delete_key(body["requestObject"]["metadata"], "managedFields") where body["requestObject"] != nil
- delete_key(body["responseObject"]["metadata"], "managedFields") where body["responseObject"] != nil
- delete_key(body["requestObject"]["metadata"]["annotations"], "kubectl.kubernetes.io/last-applied-configuration") where body["requestObject"] != nil
- delete_key(body["responseObject"]["metadata"]["annotations"], "kubectl.kubernetes.io/last-applied-configuration") where body["responseObject"] != nil
# --- маскирование (п.23): значения, а не сама команда exec ---
- replace_pattern(body["requestURI"], "(?i)(password|passwd|pwd|token|secret|api[_-]?key)(=|%3D)[^&]*", "$$1$$2***")
# --- обогащение ---
- set(body["sarex_contour"], "${env:CONTOUR}")
- set(body["sarex_dry_run"], IsMatch(body["requestURI"], "[?&]dryRun=All"))
# --- действие ---
- set(body["sarex_action"], "read") where body["verb"] == "get" or body["verb"] == "list"
- set(body["sarex_action"], "mutate") where body["verb"] == "create" or body["verb"] == "update" or body["verb"] == "patch" or body["verb"] == "delete" or body["verb"] == "deletecollection"
- set(body["sarex_action"], "status") where body["objectRef"]["subresource"] == "status"
- set(body["sarex_action"], "interactive") where IsMatch(body["objectRef"]["subresource"], "^(exec|attach|portforward)$")
# --- исход ---
- set(body["sarex_outcome"], "applied") where body["responseStatus"]["code"] >= 200 and body["responseStatus"]["code"] < 300 and body["sarex_dry_run"] == false
- set(body["sarex_outcome"], "dry_run") where body["responseStatus"]["code"] >= 200 and body["responseStatus"]["code"] < 300 and body["sarex_dry_run"] == true
- set(body["sarex_outcome"], "denied_authn") where body["responseStatus"]["code"] == 401
- set(body["sarex_outcome"], "denied_authz") where body["responseStatus"]["code"] == 403 and body["annotations"]["authorization.k8s.io/decision"] == "forbid"
- set(body["sarex_outcome"], "denied_admission") where body["responseStatus"]["code"] >= 400 and body["responseStatus"]["code"] < 500 and body["annotations"]["authorization.k8s.io/decision"] == "allow" and body["responseStatus"]["code"] != 404 and body["responseStatus"]["code"] != 409 and body["responseStatus"]["code"] != 422
- set(body["sarex_outcome"], "invalid") where body["responseStatus"]["code"] == 422
- set(body["sarex_outcome"], "conflict") where body["responseStatus"]["code"] == 409
- set(body["sarex_outcome"], "not_found") where body["responseStatus"]["code"] == 404
- set(body["sarex_outcome"], "error") where body["responseStatus"]["code"] >= 500
# --- субъект (порядок важен: сначала общее, потом частное) ---
- set(body["sarex_actor_type"], "human")
- set(body["sarex_actor_type"], "serviceaccount") where IsMatch(body["user"]["username"], "^system:serviceaccount:")
- set(body["sarex_actor_type"], "controller") where IsMatch(body["user"]["username"], "^system:serviceaccount:kube-system:") or IsMatch(body["user"]["username"], "^system:(kube-|apiserver)")
- set(body["sarex_actor_type"], "node") where IsMatch(body["user"]["username"], "^system:node:")
- set(body["sarex_actor_type"], "gitops") where IsMatch(body["user"]["username"], "^system:serviceaccount:flux-system:")
batch:
send_batch_size: 500
send_batch_max_size: 1000
timeout: 2s
exporters:
kafka/audit:
brokers:
- ${env:KAFKA_BROKERS}
protocol_version: 2.8.0
topic: security.k8s-audit
encoding: raw # в сообщение — только body (JSON события), без OTLP-обёртки
auth:
sasl:
mechanism: SCRAM-SHA-512
username: ${file:/vault/secrets/kafka-username}
password: ${file:/vault/secrets/kafka-password}
tls:
insecure: false
ca_file: /vault/secrets/kafka-ca.crt
producer:
required_acks: -1
compression: zstd
max_message_bytes: 5242880
sending_queue:
enabled: true
storage: file_storage/audit # очередь на диске: Kafka недоступна → данные не теряются
queue_size: 10000
num_consumers: 2
retry_on_failure:
enabled: true
initial_interval: 5s
max_interval: 60s
max_elapsed_time: 0 # повторять бесконечно; при полной очереди — backpressure на filelog
service:
extensions: [health_check, file_storage/audit]
pipelines:
logs/audit:
receivers: [filelog/audit]
processors: [memory_limiter, transform/audit, batch]
exporters: [kafka/audit]
telemetry:
logs:
level: info
metrics:
address: ${env:MY_POD_IP}:8888

View File

@ -0,0 +1,71 @@
# Типы событий Zitadel, на которые вешается Execution → access-collector (п.14–16 документа ИБ).
# По одному в строке, # — комментарий. Имена сверены с internal/repository/* Zitadel v4.12.2;
# фактический список инстанса Job печатает в лог (known: ...), неизвестное имя — ошибка Job'а.
# Групп пользователей (агрегат group) в v4.12.2 нет.
# создание пользователя
user.human.added
user.human.selfregistered
user.machine.added
# изменение пользователя
user.human.profile.changed
user.human.email.changed
user.human.phone.changed
user.username.changed
user.machine.changed
user.human.password.changed
# жизненный цикл: деактивация, блокировка, удаление
user.deactivated
user.reactivated
user.locked
user.unlocked
user.removed
# вход: пароль
user.human.password.check.succeeded
user.human.password.check.failed
# вход: второй фактор
user.human.mfa.otp.check.succeeded
user.human.mfa.otp.check.failed
user.human.mfa.otp.sms.check.succeeded
user.human.mfa.otp.sms.check.failed
user.human.mfa.otp.email.check.succeeded
user.human.mfa.otp.email.check.failed
user.human.mfa.u2f.token.check.succeeded
user.human.mfa.u2f.token.check.failed
user.human.passwordless.token.check.succeeded
user.human.passwordless.token.check.failed
# изменение факторов и токенов (добавление/снятие второго фактора, PAT, имперсонация)
user.human.mfa.otp.added
user.human.mfa.otp.removed
user.human.mfa.u2f.token.added
user.human.mfa.u2f.token.removed
user.human.passwordless.token.added
user.human.passwordless.token.removed
user.token.added
user.token.removed
user.impersonated
# роли (user grants)
user.grant.added
user.grant.changed
user.grant.cascade.changed
user.grant.removed
user.grant.cascade.removed
user.grant.deactivated
user.grant.reactivated
# members: инстанс (администраторы IAM), организация, проект, грант проекта
instance.member.added
instance.member.changed
instance.member.removed
instance.member.cascade.removed
org.member.added
org.member.changed
org.member.removed
org.member.cascade.removed
project.member.added
project.member.changed
project.member.removed
project.member.cascade.removed
project.grant.member.added
project.grant.member.changed
project.grant.member.removed
project.grant.member.cascade.removed

View File

@ -0,0 +1,159 @@
#!/bin/sh
# Идемпотентная настройка Zitadel Actions v2 для журнала ИБ: Target (restAsync) → access-collector
# и Execution'ы на события из EVENTS_FILE. С флагом --remove (или MODE=remove) снимает Target
# с этих Execution'ов и удаляет его.
#
# Без set -x: заголовки с PAT лежат в файле $TMP/headers и в вывод не попадают.
# Ответы API с signingKey наружу не выводятся: коллектор подпись не проверяет. При переходе на приёмник
# с проверкой HMAC ключ из ответа CreateTarget нужно будет записать во Vault, а не в лог.
#
# Код выхода ≠ 0 при любой ошибке; остальные события при этом всё равно обрабатываются.
set -eu
MODE="${MODE:-apply}"
[ "${1:-}" = "--remove" ] && MODE=remove
: "${ZITADEL_API:?ZITADEL_API не задан (http://<service>:8080)}"
: "${ZITADEL_HOST:?ZITADEL_HOST не задан (внешний домен инстанса, ExternalDomain)}"
TARGET_NAME="${TARGET_NAME:-security-audit-idp-events}"
TARGET_ENDPOINT="${TARGET_ENDPOINT:-http://access-collector.security-audit.svc.cluster.local:8088/zitadel/events}"
TARGET_TIMEOUT="${TARGET_TIMEOUT:-10s}"
EVENTS_FILE="${EVENTS_FILE:-/config/events.txt}"
PAT_FILE="${PAT_FILE:-/vault/secrets/zitadel-pat}"
CURL_MAX_TIME="${CURL_MAX_TIME:-20}"
CONNECT_ATTEMPTS="${CONNECT_ATTEMPTS:-10}"
CONNECT_PAUSE="${CONNECT_PAUSE:-15}"
ISTIO_QUIT_ON_EXIT="${ISTIO_QUIT_ON_EXIT:-false}"
TMP="${TMPDIR:-/tmp}"
# Job в mesh: istio-proxy сам не завершится, без quitquitquit под останется Running.
quit_proxy() {
if [ "$ISTIO_QUIT_ON_EXIT" = "true" ]; then
curl -fsS --max-time 5 -X POST http://127.0.0.1:15020/quitquitquit >/dev/null 2>&1 || true
fi
}
trap quit_proxy EXIT
log() { printf '%s\n' "$*"; }
err() { printf 'ОШИБКА: %s\n' "$*" >&2; }
[ -r "$PAT_FILE" ] || { err "нет файла PAT $PAT_FILE (Vault Agent)"; exit 1; }
[ -r "$EVENTS_FILE" ] || { err "нет списка событий $EVENTS_FILE"; exit 1; }
# Инстанс выбирается по x-zitadel-instance-host (InstanceHostHeaders Zitadel), Host остаётся именем Service:
# по нему istio-proxy находит маршрут и шлёт запрос с mTLS. X-Forwarded-Proto — инстанс с ExternalSecure.
umask 077
{
printf 'Authorization: Bearer %s\n' "$(tr -d '\r\n' <"$PAT_FILE")"
printf 'x-zitadel-instance-host: %s\n' "$ZITADEL_HOST"
printf 'x-zitadel-public-host: %s\n' "$ZITADEL_HOST"
printf 'X-Forwarded-Proto: https\n'
printf 'Content-Type: application/json\n'
printf 'Accept: application/json\n'
} >"$TMP/headers"
# api METHOD PATH [BODY]: тело ответа в stdout при 2xx; иначе код и текст ошибки в stderr, return 1.
api() {
_method=$1 _path=$2
if [ $# -ge 3 ]; then set -- --data "$3"; else set --; fi
_code=$(curl -sS -o "$TMP/resp" -w '%{http_code}' --max-time "$CURL_MAX_TIME" \
-X "$_method" -H @"$TMP/headers" "$@" "$ZITADEL_API$_path") || _code=000
case "$_code" in
2??) cat "$TMP/resp" ;;
*) err "$_method $_path → HTTP $_code: $(head -c 300 "$TMP/resp" 2>/dev/null | tr '\n' ' ')"; return 1 ;;
esac
}
# --- 1. Доступность API (Zitadel и коллектор поднимаются раньше Job'а, но без dependsOn) ---
attempt=1
until curl -fsS -o /dev/null --max-time "$CURL_MAX_TIME" -H @"$TMP/headers" "$ZITADEL_API/debug/ready"; do
if [ "$attempt" -ge "$CONNECT_ATTEMPTS" ]; then
err "Zitadel недоступен: $ZITADEL_API (попыток: $attempt)"; exit 1
fi
log "Zitadel не готов, попытка $attempt/$CONNECT_ATTEMPTS, пауза ${CONNECT_PAUSE}s"
attempt=$((attempt + 1))
sleep "$CONNECT_PAUSE"
done
# --- 2. Типы событий инстанса: в лог — для сверки списка, и для проверки имён ниже ---
# Ответ сначала в переменную: в конвейере api | jq код ошибки api потерялся бы (set -e смотрит на jq).
resp=$(api POST /admin/v1/events/types/_search '{}')
printf '%s' "$resp" | jq -r '.eventTypes[]?.type' | sort -u >"$TMP/known-events"
log "Типы событий Zitadel ($(wc -l <"$TMP/known-events")):"
sed 's/^/ known: /' "$TMP/known-events"
sed -e 's/#.*//' -e 's/[[:space:]]//g' "$EVENTS_FILE" | grep -v '^$' | sort -u >"$TMP/events"
# --- 3. Target ---
search=$(jq -nc --arg n "$TARGET_NAME" \
'{filters: [{targetNameFilter: {targetName: $n, method: "TEXT_FILTER_METHOD_EQUALS"}}]}')
resp=$(api POST /v2/actions/targets/search "$search")
printf '%s' "$resp" | jq -c --arg n "$TARGET_NAME" '[.targets[]? | select(.name == $n)
| {id, endpoint, timeout, async: has("restAsync")}]' >"$TMP/targets"
found=$(jq length "$TMP/targets")
if [ "$found" -gt 1 ]; then
err "Target'ов с именем $TARGET_NAME: $found, ожидался один — разберитесь вручную"; exit 1
fi
target_id=$(jq -r '.[0].id // empty' "$TMP/targets")
if [ "$MODE" = "apply" ]; then
want=$(jq -nc --arg e "$TARGET_ENDPOINT" --arg t "$TARGET_TIMEOUT" \
'{restAsync: {}, endpoint: $e, timeout: $t}')
if [ -z "$target_id" ]; then
# Из ответа берётся только id: signingKey не выводится и не сохраняется.
resp=$(api POST /v2/actions/targets "$(printf '%s' "$want" | jq -c --arg n "$TARGET_NAME" '. + {name: $n}')")
target_id=$(printf '%s' "$resp" | jq -r '.id // empty')
[ -n "$target_id" ] || { err "CreateTarget не вернул id"; exit 1; }
log "Target $TARGET_NAME: создан, id=$target_id"
elif jq -e --arg e "$TARGET_ENDPOINT" --arg t "$TARGET_TIMEOUT" \
'.[0] | .async and .endpoint == $e and .timeout == $t' "$TMP/targets" >/dev/null; then
log "Target $TARGET_NAME: без изменений, id=$target_id"
else
api POST "/v2/actions/targets/$target_id" "$want" >/dev/null
log "Target $TARGET_NAME: обновлён (тип/endpoint/timeout), id=$target_id"
fi
elif [ -z "$target_id" ]; then
log "Target $TARGET_NAME не найден — снимать нечего"; exit 0
fi
# --- 4. Execution'ы: чужие targets сохраняются, свой добавляется в конец (или убирается при --remove) ---
rc=0; n_created=0; n_extended=0; n_same=0; n_removed=0; n_err=0
while IFS= read -r ev; do
if ! grep -qxF "$ev" "$TMP/known-events"; then
log " $ev: ошибка — такого типа нет в Zitadel"; n_err=$((n_err + 1)); rc=1; continue
fi
query=$(jq -nc --arg e "$ev" '{filters: [{inConditionsFilter: {conditions: [{event: {event: $e}}]}}]}')
if ! resp=$(api POST /v2/actions/executions/search "$query"); then
log " $ev: ошибка чтения"; n_err=$((n_err + 1)); rc=1; continue
fi
current=$(printf '%s' "$resp" \
| jq -c --arg e "$ev" '[.executions[]? | select(.condition.event.event == $e) | .targets[]?]')
has=$(printf '%s' "$current" | jq --arg t "$target_id" 'index($t) != null')
if [ "$MODE" = "apply" ]; then
if [ "$has" = "true" ]; then log " $ev: без изменений"; n_same=$((n_same + 1)); continue; fi
targets=$(printf '%s' "$current" | jq -c --arg t "$target_id" '. + [$t]')
if [ "$current" = "[]" ]; then status="создано"; else status="дополнено (были другие targets)"; fi
else
if [ "$has" != "true" ]; then log " $ev: без изменений"; n_same=$((n_same + 1)); continue; fi
targets=$(printf '%s' "$current" | jq -c --arg t "$target_id" 'map(select(. != $t))')
status="target снят"
fi
body=$(jq -nc --arg e "$ev" --argjson t "$targets" '{condition: {event: {event: $e}}, targets: $t}')
if api PUT /v2/actions/executions "$body" >/dev/null; then
log " $ev: $status"
case "$status" in
создано) n_created=$((n_created + 1)) ;;
дополнено*) n_extended=$((n_extended + 1)) ;;
*) n_removed=$((n_removed + 1)) ;;
esac
else
log " $ev: ошибка записи"; n_err=$((n_err + 1)); rc=1
fi
done <"$TMP/events"
if [ "$MODE" = "remove" ] && [ "$rc" -eq 0 ]; then
api DELETE "/v2/actions/targets/$target_id" >/dev/null && log "Target $TARGET_NAME удалён, id=$target_id" || rc=1
fi
log "Итог ($MODE): target=$target_id, событий=$(wc -l <"$TMP/events"), создано=$n_created, дополнено=$n_extended, снято=$n_removed, без изменений=$n_same, ошибок=$n_err"
exit "$rc"

View File

@ -0,0 +1,91 @@
# Один раз (и после каждой правки скрипта/списка событий) настраивает Target и Execution'ы в Zitadel.
# Spec Job'а неизменяем: Flux пересоздаёт его (force). После ttlSecondsAfterFinished Job удаляется,
# и Flux создаёт его заново при следующей сверке — скрипт идемпотентен, повторный запуск ничего не меняет.
apiVersion: batch/v1
kind: Job
metadata:
name: idp-actions-bootstrap
annotations:
kustomize.toolkit.fluxcd.io/force: enabled
spec:
backoffLimit: 3
ttlSecondsAfterFinished: 86400
template:
metadata:
labels:
security-audit/job: idp-actions-bootstrap
# В mesh: запросы к Zitadel идут с mTLS (п.30). istio-proxy завершает сам скрипт (quitquitquit).
sidecar.istio.io/inject: "true"
annotations:
sidecar.istio.io/inject: "true"
proxy.istio.io/config: '{"holdApplicationUntilProxyStarts": true}'
traffic.sidecar.istio.io/excludeOutboundPorts: "8200" # Vault мимо istio-proxy (паттерн контура)
vault.hashicorp.com/agent-inject: "true"
vault.hashicorp.com/agent-init-first: "true"
vault.hashicorp.com/agent-pre-populate-only: "true"
vault.hashicorp.com/agent-requests-cpu: "25m"
vault.hashicorp.com/agent-limits-cpu: "100m"
vault.hashicorp.com/auth-path: auth/kubernetes
vault.hashicorp.com/role: security-audit-bootstrap
vault.hashicorp.com/agent-inject-secret-zitadel-pat: secrets/data/security-audit/zitadel-bootstrap
vault.hashicorp.com/agent-inject-template-zitadel-pat: |-
{{- with secret "secrets/data/security-audit/zitadel-bootstrap" -}}
{{ .Data.data.pat }}
{{- end -}}
spec:
serviceAccountName: idp-actions-bootstrap
restartPolicy: Never
imagePullSecrets:
- name: regcred
securityContext:
runAsNonRoot: true
runAsUser: 1001
runAsGroup: 1001
seccompProfile: {type: RuntimeDefault}
initContainers:
# В registry контура нет образа с curl и jq вместе: jq (1.6, Debian 12) берётся из образа kubectl,
# который уже использует setup-Job Zitadel, curl — из образа kafka (тоже Debian 12).
# TODO(verify): заменить на один toolbox-образ (generic/base-image), когда он появится.
- name: tools
image: cr.yandex/crp3ccidau046kdj8g9q/contour/zitadel/kubectl:1.33.4
command:
- sh
- -c
- cp -L /usr/bin/jq /lib/x86_64-linux-gnu/libjq.so.1 /lib/x86_64-linux-gnu/libonig.so.5 /tools/
securityContext: &container-security
runAsNonRoot: true
readOnlyRootFilesystem: true
allowPrivilegeEscalation: false
capabilities:
drop: [ALL]
resources: &resources
requests: {cpu: 10m, memory: 32Mi}
limits: {cpu: 200m, memory: 128Mi}
volumeMounts:
- {name: tools, mountPath: /tools}
containers:
- name: bootstrap
image: cr.yandex/crp3ccidau046kdj8g9q/contour/kafka-nn/kafka:4.0.0-debian-12-r10
command: ["sh", "/scripts/idp-actions-bootstrap.sh"]
env:
# ZITADEL_API и ZITADEL_HOST задаёт оверлей контура.
- {name: PATH, value: "/tools:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"}
- {name: LD_LIBRARY_PATH, value: /tools}
- {name: EVENTS_FILE, value: /config/events.txt}
- {name: PAT_FILE, value: /vault/secrets/zitadel-pat}
- {name: ISTIO_QUIT_ON_EXIT, value: "true"}
- {name: TMPDIR, value: /tmp}
securityContext: *container-security
resources: *resources
volumeMounts:
- {name: tools, mountPath: /tools, readOnly: true}
- {name: script, mountPath: /scripts, readOnly: true}
- {name: events, mountPath: /config, readOnly: true}
- {name: tmp, mountPath: /tmp}
volumes:
- {name: tools, emptyDir: {sizeLimit: 16Mi}}
- {name: tmp, emptyDir: {medium: Memory, sizeLimit: 16Mi}} # заголовки с PAT — только в памяти
- name: script
configMap: {name: idp-actions-bootstrap-script}
- name: events
configMap: {name: idp-actions-bootstrap-events}

View File

@ -0,0 +1,21 @@
# Настройка Zitadel Actions v2 (Target + Execution'ы) для потока событий IdP.
# Скрипт и список событий — через configMapGenerator: хеш в имени ConfigMap меняет spec Job'а,
# и Flux (kustomize.toolkit.fluxcd.io/force) пересоздаёт неизменяемый Job при правке любого из них.
# namespace задаёт base (security-audit).
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- serviceaccount.yaml
- networkpolicy.yaml
- job.yaml
# ${...} в скрипте — переменные sh: в контурах с postBuild.substitute Flux не должен их подставлять.
generatorOptions:
annotations:
kustomize.toolkit.fluxcd.io/substitute: disabled
configMapGenerator:
- name: idp-actions-bootstrap-script
files:
- files/idp-actions-bootstrap.sh
- name: idp-actions-bootstrap-events
files:
- files/events.txt

View File

@ -0,0 +1,30 @@
# Дополняет общую audit-collector (podSelector: {} — действует и на под Job'а: DNS, Vault, Kafka).
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: idp-actions-bootstrap
spec:
podSelector:
matchLabels:
security-audit/job: idp-actions-bootstrap
policyTypes: [Ingress, Egress]
ingress:
- ports:
- {port: 15021, protocol: TCP} # readiness istio-proxy (kubelet)
egress:
- to:
- namespaceSelector:
matchLabels: {kubernetes.io/metadata.name: zitadel}
podSelector:
matchLabels:
app.kubernetes.io/name: idp-contour
app.kubernetes.io/component: start
ports:
- {port: 8080, protocol: TCP}
- to:
- namespaceSelector:
matchLabels: {kubernetes.io/metadata.name: istio-system}
podSelector:
matchLabels: {app: istiod}
ports:
- {port: 15012, protocol: TCP} # xDS и сертификаты istio-proxy

View File

@ -0,0 +1,5 @@
# = bound SA роли Vault security-audit-bootstrap. Доступ к Kubernetes API не нужен.
apiVersion: v1
kind: ServiceAccount
metadata:
name: idp-actions-bootstrap

View File

@ -0,0 +1,11 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: security-audit
resources:
- namespace.yaml
- networkpolicy.yaml
- networkpolicy-access-collector.yaml
- authorizationpolicy-access-collector.yaml
- helmrelease-collector.yaml
- helmrelease-access-collector.yaml
- idp-actions-bootstrap

View File

@ -0,0 +1,12 @@
apiVersion: v1
kind: Namespace
metadata:
name: security-audit
labels:
# hostPath (/var/log/audit, чекпоинты) запрещён профилем baseline — нужен privileged.
pod-security.kubernetes.io/enforce: privileged
pod-security.kubernetes.io/warn: baseline
pod-security.kubernetes.io/audit: baseline
# Без istio-injection: инъекция только по метке пода sidecar.istio.io/inject=true
# (webhook object.sidecar-injector.istio.io). Метка disabled отключила бы и её.
# Sidecar получает access-collector (п.30); audit-collector — нет.

View File

@ -0,0 +1,54 @@
# Дополняет общую audit-collector (DNS, Kafka, Vault, 13133): политики складываются.
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: access-collector
spec:
podSelector:
matchLabels:
security-audit/collector: access
policyTypes: [Ingress, Egress]
ingress:
# OTLP gRPC только от ingress-шлюза. Селектор шлюза — заглушка, контур задаёт его патчем оверлея;
# без патча не совпадёт ни с одним подом (закрыто по умолчанию).
- from:
- namespaceSelector:
matchLabels: {kubernetes.io/metadata.name: istio-system}
podSelector:
matchLabels: {security-audit/gateway: unset}
ports:
- {port: 4317, protocol: TCP}
# istio-proxy: 15020 — переписанные probe'ы приложения (kubelet), 15021 — readiness самого sidecar.
- ports:
- {port: 15020, protocol: TCP}
- {port: 15021, protocol: TCP}
# События Zitadel (webhookevent) — только от подов Zitadel. Метки — чарт idp-contour, одинаковы во всех контурах.
# Правило добавлено последним: патч оверлея ссылается на /spec/ingress/0.
- from:
- namespaceSelector:
matchLabels: {kubernetes.io/metadata.name: zitadel}
podSelector:
matchLabels:
app.kubernetes.io/name: idp-contour
app.kubernetes.io/component: start
ports:
- {port: 8088, protocol: TCP}
# Журнал аудита Vault (audit-shipper в подах StatefulSet). Метки — чарт vault-contour.
- from:
- namespaceSelector:
matchLabels: {kubernetes.io/metadata.name: vault}
podSelector:
matchLabels:
app.kubernetes.io/name: vault-contour
component: server
ports:
- {port: 4319, protocol: TCP}
egress:
# xDS и сертификаты istio-proxy от istiod.
- to:
- namespaceSelector:
matchLabels: {kubernetes.io/metadata.name: istio-system}
podSelector:
matchLabels: {app: istiod}
ports:
- {port: 15012, protocol: TCP}

View File

@ -0,0 +1,32 @@
# Коллектору нужны только DNS, Kafka и Vault (агент-инжектор). Входящие — только health-check kubelet'а.
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: audit-collector
spec:
podSelector: {}
policyTypes: [Ingress, Egress]
ingress:
- ports:
- {port: 13133, protocol: TCP} # health_check (liveness/readiness)
egress:
- to:
- namespaceSelector:
matchLabels: {kubernetes.io/metadata.name: kube-system}
podSelector:
matchLabels: {k8s-app: kube-dns}
- ipBlock: {cidr: 169.254.25.10/32} # nodelocaldns kubespray; TODO(verify): enable_nodelocaldns в контуре
ports:
- {port: 53, protocol: UDP}
- {port: 53, protocol: TCP}
- to:
- namespaceSelector:
matchLabels: {kubernetes.io/metadata.name: kafka}
ports:
# client-листенер SASL_SSL: bootstrap kafka-kafka-contour и advertised-адреса подов controller-headless
- {port: 9092, protocol: TCP}
- to:
- namespaceSelector:
matchLabels: {kubernetes.io/metadata.name: vault}
ports:
- {port: 8200, protocol: TCP} # vault-vault-contour.vault.svc:8200 (AGENT_INJECT_VAULT_ADDR)

View File

@ -0,0 +1,4 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- base

View File

@ -0,0 +1,11 @@
# Ресурсы шлюза Istio. Намеренно без поля namespace: base модуля переписал бы его на security-audit,
# а этим ресурсам нужно оставаться в istio-system. Подключается из оверлея контура как resource,
# селектор шлюза (заглушка security-audit/gateway: unset) патчится там.
#
# RequestAuthentication (пользователь в журнале — x-sarex-audit-sub/org) здесь пока нет: не подтверждены все
# издатели JWT на шлюзе, а неизвестный издатель даёт 401. См. README, «Журнал доступа шлюза».
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- strip-audit-headers.yaml
- telemetry.yaml

View File

@ -0,0 +1,32 @@
# Удаляет входящие x-sarex-audit-*: клиент не должен подставлять себе пользователя в журнал доступа.
# Стоит первым HTTP-фильтром (INSERT_FIRST) — раньше jwt_authn, который заполняет эти заголовки из claims.
apiVersion: networking.istio.io/v1alpha3
kind: EnvoyFilter
metadata:
name: strip-audit-headers
namespace: istio-system
spec:
workloadSelector:
labels:
security-audit/gateway: unset
configPatches:
- applyTo: HTTP_FILTER
match:
context: GATEWAY
listener:
filterChain:
filter:
name: envoy.filters.network.http_connection_manager
patch:
operation: INSERT_FIRST
value:
name: security-audit.strip-audit-headers
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.lua.v3.Lua
default_source_code:
inline_string: |
function envoy_on_request(request_handle)
local headers = request_handle:headers()
headers:remove("x-sarex-audit-sub")
headers:remove("x-sarex-audit-org")
end

View File

@ -0,0 +1,17 @@
# Включает провайдер audit-access (meshConfig.extensionProviders в istiod) на ingress-шлюзе.
# Провайдер должен быть раскатан раньше: иначе istiod не найдёт его и журнал не включится.
apiVersion: telemetry.istio.io/v1
kind: Telemetry
metadata:
name: gateway-audit-access
namespace: istio-system
spec:
selector:
matchLabels:
security-audit/gateway: unset
accessLogging:
- providers:
- name: audit-access
# Только health-check'и. Статику не исключаем — решение за ИБ.
filter:
expression: '!(request.url_path.startsWith("/healthz") || request.url_path.startsWith("/ready"))'

View File

@ -0,0 +1,17 @@
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: access-collector
namespace: security-audit
spec:
dependsOn:
- name: kafka
namespace: kafka
- name: vault
namespace: vault
values:
extraEnvs:
- name: CONTOUR
value: uralkal
- name: KAFKA_BROKERS
value: kafka-kafka-contour.kafka.svc.cluster.local:9092

View File

@ -0,0 +1,18 @@
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: audit-collector
namespace: security-audit
spec:
dependsOn:
- name: kafka
namespace: kafka
- name: vault
namespace: vault
values:
extraEnvs:
- name: CONTOUR
value: uralkal
# Service bitnami-чарта kafka-contour (релиз kafka); тот же адрес у приложений apps/*/uralkal.
- name: KAFKA_BROKERS
value: kafka-kafka-contour.kafka.svc.cluster.local:9092

Some files were not shown because too many files have changed in this diff Show More