Compare commits

..

No commits in common. "master" and "3.9.67" have entirely different histories.

91 changed files with 111 additions and 3944 deletions

View File

@ -5,7 +5,6 @@ include:
stages: stages:
- build - build
- slices - slices
- slices-publish
variables: variables:
RELEASE_NAME: iac-offline RELEASE_NAME: iac-offline
@ -36,7 +35,7 @@ build_offline_mirror:
- docker push ${CR}/${RELEASE_NAME}:latest - docker push ${CR}/${RELEASE_NAME}:latest
timeout: 30m timeout: 30m
generate_slice_jobs: publish_contour_slices:
stage: slices stage: slices
image: ${CR}/infra/slice-builder:1.0.0 image: ${CR}/infra/slice-builder:1.0.0
tags: tags:
@ -46,20 +45,7 @@ generate_slice_jobs:
variables: variables:
GIT_DEPTH: "1" GIT_DEPTH: "1"
script: script:
- python3 ci/publish-slice.py generate --release "$CI_COMMIT_TAG" --commit "$CI_COMMIT_SHA" --out slices-pipeline.yml - printf '%s' "$SLICES_PUSHER_KEY" | base64 -d > /tmp/slices-key.json
- cat slices-pipeline.yml - python3 ci/publish-slices.py --repo . --release "$CI_COMMIT_TAG" --commit "$CI_COMMIT_SHA" --key-file /tmp/slices-key.json
artifacts: after_script:
paths: - rm -f /tmp/slices-key.json
- 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,13 +25,11 @@
| Путь | Что | | Путь | Что |
|---|---| |---|---|
| `clusters/` | 17 точек входа Flux + шаблон `contour` — **источник истины «что где раскатано»** | | `clusters/` | 10 точек входа Flux — **источник истины «что где раскатано»** |
| `infrastructure/` | 44 платформенных компонента | | `infrastructure/` | 36 платформенных компонентов |
| `apps/` | 39 прикладных сервисов | | `apps/` | 37 прикладных сервисов |
| `docs/apps/` | 34 mermaid-диаграммы по сервисам + `README.md` с доменной группировкой | | `docs/apps/` | 35 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 → имена |
@ -40,18 +38,15 @@
**Кластер.** `clusters/<cluster>/kustomization.yaml` — плоский список `../../apps/<app>/<cluster>`. Инфраструктура подключается двумя разными способами: **Кластер.** `clusters/<cluster>/kustomization.yaml` — плоский список `../../apps/<app>/<cluster>`. Инфраструктура подключается двумя разными способами:
- `d8-ugmk-prod`, `sarex-contour`, `ugok`, `uralkal`, `vad` — прямо в корневом kustomization как `../../infrastructure/<comp>/<c>`; - `d8-ugmk-prod` — прямо в корневом kustomization как `../../infrastructure/<comp>/d8-ugmk-prod`;
- `brusnika-prod`, `brusnika-stage`, `vk-infra`, `vk-prod` — через `clusters/<c>/infrastructure/kustomization.yaml`, который ссылается на оверлеи `../../../infrastructure/<comp>/<c>` (`base` + патч HelmRelease + сопутствующие манифесты компонента); - `brusnika-prod`, `brusnika-stage` — через `clusters/<c>/infrastructure/kustomization.yaml`, который ссылается на оверлеи `../../../infrastructure/<comp>/<c>` (`base` + патч HelmRelease + сопутствующие манифесты компонента);
- `wb`, `yc-cps-prod`, `yc-infra-prod`, `yc-k8s-test-02` — через `clusters/<c>/infrastructure/kustomization.yaml`, который ссылается на `../../../infrastructure/<comp>` (резолвится в `base`), а различия лежат в `clusters/<c>/infrastructure/patches/*.yaml`; - `wb` — через `clusters/wb/infrastructure/kustomization.yaml`, который ссылается на `../../../infrastructure/<comp>` (резолвится в `base`), а различия лежат в `clusters/wb/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`.
Кластеры с приложениями: `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`. Кластеры: `brusnika-prod`, `brusnika-stage`, `d8-ugmk-prod`, `wb`, `yc-cps-prod`, `yc-ecp`, `yc-infra-prod`, `yc-k8s-test`, `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`. Оверлей `contour-template` (есть у vault, istio-*, cert-manager, postgresql, kafka, rabbitmq, zitadel, camunda, istio-config) — параметризованный `${VAR}`, а не привязанный к конкретному кластеру. Чарты идут с суффиксом контура: `vault-contour`, `keycloak-contour`, `postgresql-contour` и — внимание — `idp-contour` это Zitadel. **Инфра-компонент.** `infrastructure/<comp>/base/` + оверлеи `<comp>/<cluster>/`. Корневой `infrastructure/<comp>/kustomization.yaml` всегда указывает просто на `base`. Чарты идут с суффиксом контура: `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`.
## Где искать конкретное ## Где искать конкретное
@ -67,23 +62,18 @@
| Тег образа в контуре | `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`, `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`. - **Две несовместимые конвенции оверлеев** (`IAC-CATALOG.md` §5.3.4). `d8-ugmk-prod`, `yc-k8s-test`, `dsinv` наследуют `base` и патчат его. `brusnika-prod`, `brusnika-stage`, `yc-ecp` — полные копии HelmRelease, которые молча дрейфуют от base. Правка `base` на них не влияет.
- **`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-роутинг лежит в `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`. - **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).
- **`vk-prod` маршрутизирует в namespace с суффиксом `-prod`** (`eav-prod`, `pm-prod`, `inspections-prod`), а приложений из `apps/` в этом кластере нет: сервисы раскатываются не этим репозиторием. - **Имена namespace расходятся между источниками**: `inventory.yaml` использует суффикс (`flows-prod`), `apps/` — голое имя (`flows`).
- **Имена namespace расходятся между источниками**: `inventory.yaml` и `vk-prod` используют суффикс (`flows-prod`), `apps/` — голое имя (`flows`). - **17 оверлеев приложений не подключены ни к одному кластеру** — мёртвый код.
- **66 оверлеев приложений не подключены ни к одному кластеру**: 25 — `dsinv`, 25 — закомментированные в `asterus`, остальные 16 — мёртвый код (в основном `brusnika-prod`, плюс `bi`/`iam` в `wb` и `yc-k8s-test`). - **Нет диаграмм** в `docs/apps/` для `iam`, `auth-flow`, `ams-sync`.
- **Нет диаграмм** в `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:dev5 image: cr.yandex/crp3ccidau046kdj8g9q/mc-files:dev9
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,9 +8,6 @@ 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,9 +8,6 @@ spec:
values: values:
services: services:
backend: backend:
deployment:
replicaCount:
_default: 2
envs: envs:
- name: ALLOWED_HOSTS - name: ALLOWED_HOSTS
value: value:
@ -132,6 +129,10 @@ 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"

View File

@ -65,7 +65,7 @@ spec:
enabled: false enabled: false
image: image:
name: name:
_default: cr.yandex/crp3ccidau046kdj8g9q/documentations:prod_89c8039a _default: cr.yandex/crp3ccidau046kdj8g9q/documentations:prod_fef3add2
pullPolicy: pullPolicy:
_default: IfNotPresent _default: IfNotPresent
service: service:
@ -89,9 +89,6 @@ 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"
@ -239,9 +236,6 @@ 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,9 +97,6 @@ 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"
@ -226,9 +223,6 @@ 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

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

View File

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

View File

@ -36,7 +36,7 @@ spec:
image: image:
name: name:
_default: cr.yandex/crp3ccidau046kdj8g9q/documentations:prod_0be18f51 _default: cr.yandex/crp3ccidau046kdj8g9q/documentations:prod_98a23ef5
pullPolicy: pullPolicy:
_default: IfNotPresent _default: IfNotPresent
@ -135,10 +135,6 @@ 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

@ -138,10 +138,6 @@ 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

@ -8,9 +8,6 @@ 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 = False DEBUG = True
ALLOWED_HOSTS = ['*'] ALLOWED_HOSTS = ['*']
# --------------------------------------------------------------------------------------------------------------------- # ---------------------------------------------------------------------------------------------------------------------
# DEBUG SETTINGS END # DEBUG SETTINGS END

View File

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

View File

@ -8,9 +8,6 @@ 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

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

View File

@ -1,5 +1,11 @@
--- ---
# Патч 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:
@ -56,10 +62,6 @@ 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

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

View File

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

View File

@ -1,119 +0,0 @@
---
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

@ -1,88 +0,0 @@
---
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,9 +8,6 @@ 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

@ -142,10 +142,6 @@ spec:
value: value:
_default: "0" _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,9 +8,6 @@ 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,9 +8,6 @@ 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,9 +8,6 @@ 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,9 +8,6 @@ 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_2da2a00e _default: cr.yandex/crp3ccidau046kdj8g9q/workspaces-v2-frontend:contour_a72e3265
pullPolicy: pullPolicy:
_default: IfNotPresent _default: IfNotPresent
deployment: deployment:

View File

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

View File

@ -68,7 +68,7 @@ def collect(repo: Path, start: str) -> tuple[set[PurePosixPath], list[str]]:
try: try:
return PurePosixPath(p.resolve().relative_to(repo).as_posix()) return PurePosixPath(p.resolve().relative_to(repo).as_posix())
except ValueError: except ValueError:
raise SliceError(f"reference outside the repository: {p}") raise SliceError(f"ссылка за пределы репозитория: {p}")
def add_file(p: Path): def add_file(p: Path):
r = rel(p) r = rel(p)
@ -86,21 +86,21 @@ def collect(repo: Path, start: str) -> tuple[set[PurePosixPath], list[str]]:
seen_dirs.add(d) seen_dirs.add(d)
kfile = kustomization_file(d) kfile = kustomization_file(d)
if kfile is None: if kfile is None:
raise SliceError(f"no kustomization.yaml in {rel(d)}") raise SliceError(f"нет kustomization.yaml в {rel(d)}")
if entry: if entry:
entries.append(str(rel(d))) entries.append(str(rel(d)))
add_file(kfile) add_file(kfile)
doc = yaml.safe_load(kfile.read_text()) or {} doc = yaml.safe_load(kfile.read_text()) or {}
for ref in local_refs(doc): for ref in local_refs(doc):
if is_remote(ref): if is_remote(ref):
raise SliceError(f"remote reference {ref} in {rel(kfile)}") raise SliceError(f"удалённая ссылка {ref} в {rel(kfile)}")
target = (d / ref) target = (d / ref)
if target.is_dir(): if target.is_dir():
visit_dir(target) visit_dir(target)
elif target.is_file(): elif target.is_file():
add_file(target) add_file(target)
else: else:
raise SliceError(f"reference {ref} in {rel(kfile)} does not exist") raise SliceError(f"ссылка {ref} в {rel(kfile)} никуда не ведёт")
visit_dir(repo / start, entry=True) visit_dir(repo / start, entry=True)
return files, sorted(set(entries)) return files, sorted(set(entries))
@ -124,7 +124,7 @@ def write_tar(repo: Path, files: set[PurePosixPath], out: Path):
def kustomize_build(path: Path) -> bytes: def kustomize_build(path: Path) -> bytes:
p = subprocess.run(["kubectl", "kustomize", str(path)], capture_output=True) p = subprocess.run(["kubectl", "kustomize", str(path)], capture_output=True)
if p.returncode != 0: if p.returncode != 0:
raise SliceError(f"kustomize build {path} failed: {p.stderr.decode().strip()}") raise SliceError(f"kustomize build {path}: {p.stderr.decode().strip()}")
return p.stdout return p.stdout
@ -135,7 +135,7 @@ def verify(repo: Path, out: Path, entries: list[str]):
tar.extractall(tmp, filter="data") tar.extractall(tmp, filter="data")
for e in entries: for e in entries:
if kustomize_build(repo / e) != kustomize_build(Path(tmp) / e): if kustomize_build(repo / e) != kustomize_build(Path(tmp) / e):
raise SliceError(f"kustomize build of {e} differs between slice and full repository") raise SliceError(f"build {e} в срезе отличается от полного репозитория")
def main() -> int: def main() -> int:
@ -153,9 +153,9 @@ def main() -> int:
if not args.no_verify: if not args.no_verify:
verify(repo, out, entries) verify(repo, out, entries)
except SliceError as e: except SliceError as e:
print(f"slice {args.contour}: {e}", file=sys.stderr) print(f"срез {args.contour}: {e}", file=sys.stderr)
return 1 return 1
print(f"slice {args.contour}: {len(files)} files, entry points: {', '.join(entries)}") print(f"срез {args.contour}: {len(files)} файлов, слоёв {len(entries)}: {', '.join(entries)}")
return 0 return 0

View File

@ -4,10 +4,10 @@ import base64
import hashlib import hashlib
import importlib.util import importlib.util
import json import json
import os
import re import re
import sys import sys
import tempfile import tempfile
import time
import urllib.error import urllib.error
import urllib.parse import urllib.parse
import urllib.request import urllib.request
@ -22,10 +22,10 @@ spec.loader.exec_module(bs)
REGISTRY_HOST = "cr.yandex" REGISTRY_HOST = "cr.yandex"
SLICE_REGISTRY = "crpct2b2lsmao4nm0cjk" SLICE_REGISTRY = "crpct2b2lsmao4nm0cjk"
INSTALLER_GITEA_URL = "http://gitea.gitea.svc:3000/infra/iac.git"
CONFIG_TYPE = "application/vnd.sarex.slice.config.v1+json" CONFIG_TYPE = "application/vnd.sarex.slice.config.v1+json"
LAYER_TYPE = "application/vnd.oci.image.layer.v1.tar" LAYER_TYPE = "application/vnd.oci.image.layer.v1.tar"
MANIFEST_TYPE = "application/vnd.oci.image.manifest.v1+json" MANIFEST_TYPE = "application/vnd.oci.image.manifest.v1+json"
JOB_IMAGE = "${CR}/infra/slice-builder:1.0.0"
class PublishError(Exception): class PublishError(Exception):
@ -36,28 +36,13 @@ def digest(data: bytes) -> str:
return "sha256:" + hashlib.sha256(data).hexdigest() return "sha256:" + hashlib.sha256(data).hexdigest()
def list_contours(repo: Path) -> list[str]: def connected_contours(repo: Path) -> list[str]:
return sorted(p.parent.name for p in repo.glob("clusters/*/kustomization.yaml")) found = []
for sync in sorted(repo.glob("clusters/*/flux-system/gotk-sync.yaml")):
for doc in yaml.safe_load_all(sync.read_text()):
def child_pipeline(contours: list[str], release: str, commit: str) -> str: if isinstance(doc, dict) and doc.get("kind") == "GitRepository" and doc.get("spec", {}).get("url") == INSTALLER_GITEA_URL:
jobs: dict = {"stages": ["slices"]} found.append(sync.parents[1].name)
for c in contours: return found
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]: def build_manifest(layer: bytes, annotations: dict[str, str]) -> tuple[bytes, bytes]:
@ -97,7 +82,7 @@ class Registry:
params["scope"] = scope params["scope"] = scope
status, _, body = self._send("GET", f"{realm}?{urllib.parse.urlencode(params)}", headers={"Authorization": self.basic}) status, _, body = self._send("GET", f"{realm}?{urllib.parse.urlencode(params)}", headers={"Authorization": self.basic})
if status != 200: if status != 200:
raise PublishError(f"registry auth failed: HTTP {status}") raise PublishError(f"авторизация в реестре: HTTP {status}")
payload = json.loads(body) payload = json.loads(body)
return payload.get("token") or payload.get("access_token") return payload.get("token") or payload.get("access_token")
@ -123,94 +108,70 @@ class Registry:
raise PublishError(f"{repo}:current: HTTP {status}") raise PublishError(f"{repo}:current: HTTP {status}")
return json.loads(body)["layers"][0]["digest"] return json.loads(body)["layers"][0]["digest"]
def put_blob(self, repo: str, data: bytes) -> bool: def put_blob(self, repo: str, data: bytes):
d = digest(data) d = digest(data)
if self.request("HEAD", repo, f"blobs/{d}")[0] == 200: if self.request("HEAD", repo, f"blobs/{d}")[0] == 200:
return False return
status, h, _ = self.request("POST", repo, "blobs/uploads/") status, h, _ = self.request("POST", repo, "blobs/uploads/")
if status != 202: if status != 202:
raise PublishError(f"blob upload to {repo}: HTTP {status}") raise PublishError(f"загрузка блоба в {repo}: HTTP {status}")
location = h["location"] location = h["location"]
if location.startswith("/"): if location.startswith("/"):
location = self.base + location location = self.base + location
sep = "&" if "?" in location else "?" sep = "&" if "?" in location else "?"
status, _, _ = self.request("PUT", repo, f"{location}{sep}digest={d}", data, {"Content-Type": "application/octet-stream"}) status, _, _ = self.request("PUT", repo, f"{location}{sep}digest={d}", data, {"Content-Type": "application/octet-stream"})
if status != 201: if status != 201:
raise PublishError(f"blob upload to {repo}: HTTP {status}") raise PublishError(f"загрузка блоба в {repo}: HTTP {status}")
return True
def push(self, repo: str, tags: list[str], layer: bytes, annotations: dict[str, str]) -> bool: def push(self, repo: str, tags: list[str], layer: bytes, annotations: dict[str, str]):
config, manifest = build_manifest(layer, annotations) config, manifest = build_manifest(layer, annotations)
self.put_blob(repo, config) self.put_blob(repo, config)
uploaded = self.put_blob(repo, layer) self.put_blob(repo, layer)
for tag in tags: for tag in tags:
status, _, body = self.request("PUT", repo, f"manifests/{tag}", manifest, {"Content-Type": MANIFEST_TYPE}) status, _, body = self.request("PUT", repo, f"manifests/{tag}", manifest, {"Content-Type": MANIFEST_TYPE})
if status != 201: if status != 201:
raise PublishError(f"{repo}:{tag}: HTTP {status} {body[:200]!r}") 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: def main() -> int:
ap = argparse.ArgumentParser() ap = argparse.ArgumentParser()
sub = ap.add_subparsers(dest="cmd", required=True) ap.add_argument("--repo", default=".")
gen = sub.add_parser("generate") ap.add_argument("--release", required=True)
gen.add_argument("--repo", default=".") ap.add_argument("--commit", required=True)
gen.add_argument("--release", required=True) ap.add_argument("--branch", default="")
gen.add_argument("--commit", required=True) ap.add_argument("--key-file", 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() args = ap.parse_args()
repo = Path(args.repo).resolve() repo = Path(args.repo).resolve()
if args.cmd == "generate": registry = Registry("json_key", Path(args.key_file).read_text())
contours = list_contours(repo) contours = connected_contours(repo)
Path(args.out).write_text(child_pipeline(contours, args.release, args.commit)) print(f"подключённые контуры: {', '.join(contours) or 'нет'}")
log(f"{len(contours)} contours: {', '.join(contours)}") failed = []
return 0 for contour in contours:
return publish(repo, args.contour, args.release, args.commit, Path(args.key_file)) target = f"{SLICE_REGISTRY}/{contour}-iac"
try:
with tempfile.TemporaryDirectory() as tmp:
out = Path(tmp) / "slice.tar"
files, entries = bs.collect(repo, f"clusters/{contour}")
bs.write_tar(repo, files, out)
bs.verify(repo, out, entries)
layer = out.read_bytes()
changed = registry.current_layer(target) != digest(layer)
tags = [args.commit[:8], args.release] + (["current"] if changed else [])
registry.push(target, tags, layer, {
"org.opencontainers.image.source": "iac",
"org.opencontainers.image.revision": args.commit,
"org.opencontainers.image.version": args.release,
"org.opencontainers.image.created": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
"io.sarex.slice.branch": args.branch,
"io.sarex.slice.contour": contour,
"io.sarex.slice.kind": "iac",
})
state = "current переставлен" if changed else "содержимое как в current"
print(f"{target}: {len(files)} файлов, {digest(layer)[:19]}, теги {', '.join(tags)}, {state}")
except (bs.SliceError, PublishError) as e:
print(f"{target}: ОШИБКА {e}", file=sys.stderr)
failed.append(contour)
return 1 if failed else 0
if __name__ == "__main__": if __name__ == "__main__":

View File

@ -3,7 +3,6 @@ import tarfile
from pathlib import Path from pathlib import Path
import pytest import pytest
import yaml
spec = importlib.util.spec_from_file_location("build_slice", Path(__file__).parents[1] / "build-slice.py") spec = importlib.util.spec_from_file_location("build_slice", Path(__file__).parents[1] / "build-slice.py")
bs = importlib.util.module_from_spec(spec) bs = importlib.util.module_from_spec(spec)
@ -49,13 +48,13 @@ def test_slice_follows_flux_paths_and_skips_foreign(repo):
def test_remote_reference_stops_build(repo): def test_remote_reference_stops_build(repo):
put(repo, "clusters/a/kustomization.yaml", "resources:\n - https://github.com/x/y//z\n") put(repo, "clusters/a/kustomization.yaml", "resources:\n - https://github.com/x/y//z\n")
with pytest.raises(bs.SliceError, match="remote reference"): with pytest.raises(bs.SliceError, match="удалённая ссылка"):
bs.collect(repo, "clusters/a") bs.collect(repo, "clusters/a")
def test_dangling_reference_stops_build(repo): def test_dangling_reference_stops_build(repo):
put(repo, "clusters/a/kustomization.yaml", "resources:\n - ./missing.yaml\n") put(repo, "clusters/a/kustomization.yaml", "resources:\n - ./missing.yaml\n")
with pytest.raises(bs.SliceError, match="does not exist"): with pytest.raises(bs.SliceError, match="никуда не ведёт"):
bs.collect(repo, "clusters/a") bs.collect(repo, "clusters/a")
@ -71,23 +70,16 @@ def test_tar_is_byte_identical_across_runs(repo, tmp_path_factory):
assert all(m.mtime == 0 for m in t.getmembers()) assert all(m.mtime == 0 for m in t.getmembers())
pspec = importlib.util.spec_from_file_location("publish_slices", Path(__file__).parents[1] / "publish-slices.py")
pspec = importlib.util.spec_from_file_location("publish_slice", Path(__file__).parents[1] / "publish-slice.py")
ps = importlib.util.module_from_spec(pspec) ps = importlib.util.module_from_spec(pspec)
pspec.loader.exec_module(ps) pspec.loader.exec_module(ps)
def test_every_cluster_with_kustomization_is_a_contour(tmp_path): def test_only_installer_contours_are_connected(tmp_path):
put(tmp_path, "clusters/stand/kustomization.yaml", "resources: []\n") put(tmp_path, "clusters/stand/flux-system/gotk-sync.yaml",
put(tmp_path, "clusters/vad/kustomization.yaml", "resources: []\n") f"apiVersion: source.toolkit.fluxcd.io/v1\nkind: GitRepository\nspec:\n url: {ps.INSTALLER_GITEA_URL}\n")
put(tmp_path, "clusters/contour/apps.yaml", "kind: Kustomization\n") put(tmp_path, "clusters/vad/flux-system/gotk-sync.yaml",
assert ps.list_contours(tmp_path) == ["stand", "vad"] "apiVersion: source.toolkit.fluxcd.io/v1\nkind: GitRepository\nspec:\n url: http://192.168.8.141:31361/infra/iac.git\n")
put(tmp_path, "clusters/old/flux-system/gotk-sync.yaml",
"apiVersion: source.toolkit.fluxcd.io/v1\nkind: GitRepository\nspec:\n url: https://gitlab.sarex.io/infra/iac.git\n")
def test_child_pipeline_has_one_job_per_contour(): assert ps.connected_contours(tmp_path) == ["stand"]
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

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

View File

@ -1,7 +0,0 @@
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

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

View File

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

View File

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

View File

@ -3,11 +3,3 @@ kind: Kustomization
resources: resources:
- gotk-components.yaml - gotk-components.yaml
- gotk-sync.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

@ -7,8 +7,6 @@ metadata:
spec: spec:
type: oci type: oci
interval: 10m0s interval: 10m0s
url: ${CHART_REPO:=oci://cr.yandex/crp3ccidau046kdj8g9q/charts} url: oci://cr.yandex/crp3ccidau046kdj8g9q/charts
secretRef: secretRef:
name: ${CHART_REPO_SECRET:=yc-cr-auth} name: yc-cr-auth
certSecretRef:
name: chart-repo-ca

View File

@ -2,4 +2,4 @@ apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization kind: Kustomization
resources: resources:
- ./flux-system - ./flux-system
- ./layers.yaml - ./helm-repositories.yaml

View File

@ -1,93 +0,0 @@
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,31 +54,4 @@ 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

@ -1,354 +0,0 @@
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

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

View File

@ -1,25 +0,0 @@
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

@ -1,28 +0,0 @@
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

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

View File

@ -1,117 +0,0 @@
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

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

View File

@ -103,17 +103,6 @@ spec:
renewBefore: 360h0m0s renewBefore: 360h0m0s
privateKey: privateKey:
rotationPolicy: Always 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:
@ -183,17 +172,6 @@ spec:
tls: tls:
credentialName: wiki-tls credentialName: wiki-tls
mode: SIMPLE 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
@ -272,14 +250,3 @@ spec:
prefix: / prefix: /
service: outline.outline.svc.cluster.local service: outline.outline.svc.cluster.local
port: 80 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

@ -1,31 +0,0 @@
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

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

View File

@ -1,12 +0,0 @@
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

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

View File

@ -13,34 +13,3 @@ 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

@ -1,73 +0,0 @@
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

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

View File

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

View File

@ -1,104 +0,0 @@
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

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

View File

@ -1,104 +0,0 @@
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

@ -1,690 +0,0 @@
# 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

@ -1,34 +0,0 @@
# Транспортная аутентификация вместо 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

@ -1,270 +0,0 @@
# Журнал доступа 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

@ -1,245 +0,0 @@
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

@ -1,71 +0,0 @@
# Типы событий 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

@ -1,159 +0,0 @@
#!/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

@ -1,91 +0,0 @@
# Один раз (и после каждой правки скрипта/списка событий) настраивает 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

@ -1,21 +0,0 @@
# Настройка 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

@ -1,30 +0,0 @@
# Дополняет общую 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

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

View File

@ -1,11 +0,0 @@
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

@ -1,12 +0,0 @@
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

@ -1,54 +0,0 @@
# Дополняет общую 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

@ -1,32 +0,0 @@
# Коллектору нужны только 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

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

View File

@ -1,11 +0,0 @@
# Ресурсы шлюза 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

@ -1,32 +0,0 @@
# Удаляет входящие 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

@ -1,17 +0,0 @@
# Включает провайдер 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

@ -1,17 +0,0 @@
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

@ -1,18 +0,0 @@
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

View File

@ -1,17 +0,0 @@
# Zitadel контура: infrastructure/zitadel/uralkal (ExternalDomain, ExternalSecure: true), Service чарта idp-contour.
apiVersion: batch/v1
kind: Job
metadata:
name: idp-actions-bootstrap
spec:
template:
spec:
containers:
- name: bootstrap
env:
- name: ZITADEL_API
value: http://zitadel-idp-contour.zitadel.svc.cluster.local:8080
# Инстанс по x-zitadel-instance-host. TODO(verify): запрос с этим заголовком к Service проходит
# (см. README, «События Zitadel» → проверка API изнутри кластера).
- name: ZITADEL_HOST
value: login.sarex.local.uralkali.com

View File

@ -1,32 +0,0 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ../base
- ../mesh # istio-system; применять после раскатки провайдера audit-access в istio-pilot/uralkal
patches:
- path: audit-collector.yaml
- path: access-collector.yaml
- path: idp-actions-bootstrap.yaml
# Селектор ingress-шлюза (HelmRelease istio-system/ingressgateway): replace, а не merge —
# merge-patch CRD оставил бы и заглушку security-audit/gateway.
- target:
kind: EnvoyFilter
name: strip-audit-headers
patch: |-
- op: replace
path: /spec/workloadSelector/labels
value: {istio: ingressgateway}
- target:
kind: Telemetry
name: gateway-audit-access
patch: |-
- op: replace
path: /spec/selector/matchLabels
value: {istio: ingressgateway}
- target:
kind: NetworkPolicy
name: access-collector
patch: |-
- op: replace
path: /spec/ingress/0/from/0/podSelector/matchLabels
value: {istio: ingressgateway}

View File

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

View File

@ -1,110 +0,0 @@
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: vault
namespace: vault
spec:
interval: 5m
timeout: 10m
install:
disableWait: true
upgrade:
disableWait: true
postRenderers:
- kustomize:
patches:
- target:
group: apps
version: v1
kind: StatefulSet
namespace: vault
name: vault-vault-contour
patch: |-
- op: replace
path: /spec/template/spec/nodeSelector
value:
dedicated: generic
- op: replace
path: /spec/template/spec/tolerations
value: []
values:
global:
namespace: vault
imagePullSecrets:
- name: regcred
backup:
enabled: false
injector:
nodeSelector:
dedicated: generic
tolerations: []
server:
includeConfigAnnotation: true
nodeSelector:
dedicated: generic
tolerations: []
dataStorage:
size: 10Gi
storageClass: ${STORAGE_CLASS}
volumes:
- name: unseal-keys
secret:
secretName: vault-unseal-keys
optional: true
defaultMode: 0440
extraContainers:
- name: unsealer
image: cr.yandex/crp3ccidau046kdj8g9q/vault/vault:1.21.2
imagePullPolicy: IfNotPresent
env:
- name: VAULT_ADDR
value: http://127.0.0.1:8200
command:
- /bin/sh
- -c
- |
while true; do
if vault status >/dev/null 2>&1; then
:
else
rc=$?
if [ "$rc" -eq 2 ]; then
if [ -s /vault/unseal-keys/key1 ] && [ -s /vault/unseal-keys/key2 ] && [ -s /vault/unseal-keys/key3 ]; then
for k in key1 key2 key3; do
vault operator unseal "$(cat /vault/unseal-keys/$k)" >/dev/null 2>&1 || echo "unseal call failed"
done
if vault status >/dev/null 2>&1; then echo "unsealed"; else echo "still sealed"; fi
else
echo "sealed, keys not mounted"
fi
fi
fi
sleep 10
done
volumeMounts:
- name: unseal-keys
mountPath: /vault/unseal-keys
readOnly: true
resources:
requests:
cpu: 10m
memory: 16Mi
limits:
memory: 64Mi
ha:
replicas: 1
raft:
config: |
ui = true
listener "tcp" {
tls_disable = 1
address = "[::]:8200"
cluster_address = "[::]:8201"
}
storage "raft" {
path = "/vault/data"
}
service_registration "kubernetes" {}

View File

@ -1,56 +0,0 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: audit-rotate-script
namespace: vault
data:
rotate.sh: |
#!/bin/sh
# Ротация журнала аудита Vault по размеру. POSIX sh (busybox), без set -x и без set -e:
# ошибка одной итерации не должна валить sidecar. В лог — только размер и число файлов, ничего из журнала.
#
# filelog в audit-shipper читает audit.log* и отслеживает файлы по fingerprint (первые байты),
# поэтому переименованный файл дочитывается. KEEP должен перекрывать максимальное отставание
# shipper'а: следить по метрикам otelcol и алерту на отставание (README).
# Каталог .otelcol (чекпоинты и очередь) не трогается.
set -u
LOG="${AUDIT_LOG:-/vault/audit/audit.log}"
MAX_BYTES="${MAX_BYTES:-104857600}"
KEEP="${KEEP:-5}"
INTERVAL="${INTERVAL:-30}"
rotate() {
# pid проверяется ДО переименования: без SIGHUP Vault продолжит писать в audit.log.1, новый audit.log
# не появится, ротация остановится, а файл будет расти до заполнения диска (Vault fail-closed).
pid=$(pidof vault)
case "$pid" in
'' | *' '*)
echo "rotate: ОШИБКА: pid vault не найден или их несколько ('$pid'), ротация пропущена"
return 1
;;
esac
i=$KEEP
while [ "$i" -gt 1 ]; do
prev=$((i - 1))
if [ -e "$LOG.$prev" ]; then mv -f "$LOG.$prev" "$LOG.$i"; fi
i=$prev
done
mv -f "$LOG" "$LOG.1" || return 1
# Vault переоткрывает файл по SIGHUP; до этого события пишутся в audit.log.1 и не теряются.
kill -HUP "$pid" || echo "rotate: ОШИБКА: SIGHUP pid $pid не доставлен"
for f in "$LOG".*; do
n=${f##*.}
case "$n" in '' | *[!0-9]*) continue ;; esac
if [ "$n" -gt "$KEEP" ]; then rm -f "$f"; fi
done
echo "rotate: size=$1 files=$(ls -1 "$LOG"* 2>/dev/null | wc -l)"
}
trap 'exit 0' TERM INT
while :; do
size=$(stat -c %s "$LOG" 2>/dev/null || echo 0)
if [ "$size" -gt "$MAX_BYTES" ]; then rotate "$size"; fi
# sleep в фоне + wait: SIGTERM при остановке пода обрабатывается сразу, а не после sleep.
sleep "$INTERVAL" &
wait $!
done

View File

@ -1,68 +0,0 @@
# Конфиг sidecar'а audit-shipper. Без configMapGenerator намеренно: смена конфига не должна сама
# перезапускать Vault (рестарт = unseal). Изменения применяются ручным рестартом пода по процедуре README.
apiVersion: v1
kind: ConfigMap
metadata:
name: audit-shipper-config
namespace: vault
data:
config.yaml: |
extensions:
file_storage/vault:
directory: /vault/audit/.otelcol
create_directory: true
compaction:
on_start: true
on_rebound: true # очередь bbolt иначе не уменьшается на диске
directory: /vault/audit/.otelcol
receivers:
filelog/vault:
include: [/vault/audit/audit.log*]
exclude: [/vault/audit/.otelcol/*]
start_at: beginning
storage: file_storage/vault
max_log_size: 4MiB
operators:
- type: json_parser
parse_to: body
processors:
memory_limiter: {check_interval: 1s, limit_mib: 150, spike_limit_mib: 50}
transform/vault:
error_mode: ignore
log_statements:
- context: log
statements:
# response-запись содержит и запрос — request-записи отбрасываем (объём ~вдвое меньше)
- set(body["sarex_drop"], true) where body["type"] == "request"
- set(body["sarex_kind"], "vault_audit")
- set(body["sarex_contour"], "${env:CONTOUR}")
- set(body["sarex_outcome"], "applied")
- set(body["sarex_outcome"], "denied_authz") where IsMatch(body["error"], "permission denied")
- set(body["sarex_outcome"], "denied_authn") where body["error"] != nil and body["error"] != "" and IsMatch(body["request"]["path"], "^auth/.+/login")
- set(body["sarex_actor_type"], "serviceaccount") where body["auth"]["metadata"]["service_account_name"] != nil
- set(body["sarex_actor_type"], "human") where body["sarex_actor_type"] == nil
filter/vault:
logs:
log_record: ['body["sarex_drop"] == true']
batch: {send_batch_size: 500, timeout: 2s}
exporters:
otlp/audit:
endpoint: access-collector.security-audit.svc.cluster.local:4319
# Vault и access-collector в mesh: plaintext до istio-proxy пода, дальше auto-mTLS.
# TODO(verify): у подов Vault есть istio-proxy (ns vault: istio-injection=enabled).
tls: {insecure: true}
# Очередь на диске ограничена: 20000 батчей по 500 записей не помещаются в PVC, а полный диск
# останавливает Vault. При заполненной очереди filelog перестаёт читать — буфером служат сами файлы
# журнала (KEEP × MAX_BYTES). TODO(verify): queue_size по замеру объёма.
sending_queue: {enabled: true, storage: file_storage/vault, queue_size: 1000}
retry_on_failure: {enabled: true, max_elapsed_time: 0}
service:
extensions: [file_storage/vault]
pipelines:
logs:
receivers: [filelog/vault]
processors: [memory_limiter, transform/vault, filter/vault, batch]
exporters: [otlp/audit]
telemetry:
logs: {level: info}
metrics: {address: "${env:MY_POD_IP}:8888"}

View File

@ -1,20 +0,0 @@
# Vault Уралкалия + журнал аудита (file device → sidecar'ы → access-collector). НЕ ПОДКЛЮЧЁН.
# Включение — замена ../../infrastructure/vault/uralkal на ../../infrastructure/vault/uralkal-audit
# в clusters/uralkal/kustomization.yaml по процедуре из infrastructure/security-audit/README.md
# («Журнал аудита Vault»): VCT меняется → StatefulSet пересоздаётся с --cascade=orphan, поды — unseal.
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ../uralkal
# PVC отдельным файлом не нужен: 3 реплики (HA raft) → том через server.auditStorage (volumeClaimTemplates).
- configmap-audit-shipper.yaml
- configmap-audit-rotate.yaml
patches:
# JSON6902 только в spec.values: всё нужное чарт vault-contour 0.2.3 умеет через values.
# spec.postRenderers (nodeSelector/tolerations из ../uralkal) не трогается.
- path: vault-audit-patch.yaml
target:
group: helm.toolkit.fluxcd.io
kind: HelmRelease
name: vault
namespace: vault

View File

@ -1,70 +0,0 @@
# Журнал аудита Vault: том /vault/audit, общий PID namespace и два sidecar'а.
# Чарт vault-contour 0.2.3: auditStorage → volumeClaimTemplates "audit" + mount в контейнер vault;
# extraContainers/volumes/shareProcessNamespace — values (toYaml, без tpl).
#
# Vault — fail-closed: включённый audit device, который не может записать событие, останавливает обслуживание
# запросов. Поэтому сеть в пути записи нет (файл + sidecar), а объём на диске ограничен ротацией и очередью.
- op: add
path: /spec/values/server/auditStorage
value:
enabled: true
size: 5Gi # TODO(verify): по замеру объёма журнала (KEEP × MAX_BYTES + очередь + запас)
storageClass: local-path
accessMode: ReadWriteOnce
mountPath: /vault/audit
# Нужен audit-rotate для SIGHUP процессу vault. Побочный эффект: sidecar'ы с UID vault видят его процесс
# (/proc/<pid>/environ; память — при разрешающем kernel.yama.ptrace_scope). См. README, «Решения с последствиями».
- op: add
path: /spec/values/server/shareProcessNamespace
value: true
- op: add
path: /spec/values/server/volumes
value:
- name: audit-shipper-config
configMap: {name: audit-shipper-config}
- name: audit-rotate-script
configMap: {name: audit-rotate-script}
- op: add
path: /spec/values/server/extraContainers
value:
# filelog → OTLP в access-collector. Чекпоинты и очередь отправки — на том же PVC.
# Кредов Kafka здесь нет: Vault Agent в поде Vault невозможен (Vault стартует запечатанным).
- name: audit-shipper
image: cr.yandex/crp3ccidau046kdj8g9q/opentelemetry-collector/opentelemetry-collector-contrib:0.121.0-amd64
args: ["--config=/conf/config.yaml"]
env:
- {name: CONTOUR, value: uralkal}
- name: MY_POD_IP
valueFrom: {fieldRef: {fieldPath: status.podIP}}
- {name: GOMEMLIMIT, value: 200MiB}
securityContext: &audit-sidecar-security
# UID/GID процесса vault (podSecurityContext чарта: 100/1000): файл журнала 0600, владелец — vault.
runAsUser: 100
runAsGroup: 1000
runAsNonRoot: true
readOnlyRootFilesystem: true
allowPrivilegeEscalation: false
capabilities: {drop: [ALL]}
seccompProfile: {type: RuntimeDefault}
resources:
requests: {cpu: 25m, memory: 64Mi}
limits: {cpu: 200m, memory: 256Mi}
volumeMounts:
- {name: audit, mountPath: /vault/audit}
- {name: audit-shipper-config, mountPath: /conf, readOnly: true}
# Ротация по размеру + SIGHUP vault (file device по SIGHUP переоткрывает файл).
- name: audit-rotate
# busybox 1.37.0 из registry контура (helperImage local-path-provisioner); тег latest закреплён digest'ом.
image: cr.yandex/crp3ccidau046kdj8g9q/contour/local-path-provisioner-nn/busybox@sha256:11f85134f388cff5f4c66f9bb4c5942249c1f6f7eb8b3889948d953487b5f7a8
command: ["/bin/sh", "/scripts/rotate.sh"]
env:
- {name: MAX_BYTES, value: "104857600"}
- {name: KEEP, value: "5"}
- {name: INTERVAL, value: "30"}
securityContext: *audit-sidecar-security # тот же UID: иначе нельзя отправить сигнал vault
resources:
requests: {cpu: 5m, memory: 16Mi}
limits: {cpu: 50m, memory: 32Mi}
volumeMounts:
- {name: audit, mountPath: /vault/audit}
- {name: audit-rotate-script, mountPath: /scripts, readOnly: true}

View File

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

View File

@ -1,168 +0,0 @@
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: zitadel
namespace: zitadel
spec:
interval: 5m
timeout: 10m
dependsOn:
- name: postgresql
namespace: postgresql
postRenderers:
- kustomize:
patches:
- target:
group: apps
version: v1
kind: Deployment
name: zitadel-idp-contour
patch: |-
- op: add
path: /spec/template/spec/nodeSelector
value:
dedicated: generic
- op: add
path: /spec/template/spec/tolerations
value: []
- op: replace
path: /spec/template/metadata/annotations/vault.hashicorp.com~1agent-inject-template-zitadel-vault-config.yaml
value: |-
{{- with secret "secrets/data/zitadel/postgresql" -}}
Database:
postgres:
User:
Password: |-
{{ index .Data.data "password" }}
Admin:
Password: |-
{{ index .Data.data "password" }}
FirstInstance:
Org:
Human:
Password: |-
{{ index .Data.data "humanPassword" }}
{{- end -}}
- target:
group: batch
version: v1
kind: Job
name: zitadel-idp-contour-init
patch: |-
- op: add
path: /spec/template/spec/nodeSelector
value:
dedicated: generic
- op: add
path: /spec/template/spec/tolerations
value: []
- op: replace
path: /spec/template/metadata/annotations/vault.hashicorp.com~1agent-inject-template-zitadel-vault-config.yaml
value: |-
{{- with secret "secrets/data/zitadel/postgresql" -}}
Database:
postgres:
User:
Password: |-
{{ index .Data.data "password" }}
Admin:
Password: |-
{{ index .Data.data "password" }}
FirstInstance:
Org:
Human:
Password: |-
{{ index .Data.data "humanPassword" }}
{{- end -}}
- target:
group: batch
version: v1
kind: Job
name: zitadel-idp-contour-setup
patch: |-
- op: add
path: /spec/template/spec/nodeSelector
value:
dedicated: generic
- op: add
path: /spec/template/spec/tolerations
value: []
- op: replace
path: /spec/template/metadata/annotations/vault.hashicorp.com~1agent-inject-template-zitadel-vault-config.yaml
value: |-
{{- with secret "secrets/data/zitadel/postgresql" -}}
Database:
postgres:
User:
Password: |-
{{ index .Data.data "password" }}
Admin:
Password: |-
{{ index .Data.data "password" }}
FirstInstance:
Org:
Human:
Password: |-
{{ index .Data.data "humanPassword" }}
{{- end -}}
values:
zitadel:
configmapConfig:
ExternalDomain: ${HOST_LOGIN}
ExternalSecure: true
# IAM_OWNER service user for sarexctl phase zitadel-setup; the chart stores its key
# in secret zitadel/sarex-installer. Created only on the first instance setup.
FirstInstance:
Org:
Machine:
Machine:
Username: sarex-installer
Name: sarexctl
MachineKey:
ExpirationDate: "2099-01-01T00:00:00Z"
Type: 1
debug:
enabled: false
postgresqlSecret:
create: false
vault:
enabled: true
role: zitadel
authPath: auth/kubernetes
secretPath: secrets/data/zitadel/postgresql
secretKey: password
kvVersion: 2
fileName: zitadel-vault-config.yaml
serviceAccount:
create: true
name: zitadel-idp-contour
replicaCount: 1
pdb:
enabled: false
env:
- name: ZITADEL_DEFAULTINSTANCE_FEATURES_LOGINV2_REQUIRED
value: "false"
- name: ZITADEL_SYSTEMDEFAULTS_PASSWORDHASHER_VERIFIERS
value: "bcrypt,pbkdf2"
- name: ZITADEL_MACHINE_IDENTIFICATION_HOSTNAME_ENABLED
value: "true"
- name: ZITADEL_DATABASE_POSTGRES_HOST
value: "postgresql.postgresql.svc.cluster.local"
- name: ZITADEL_DATABASE_POSTGRES_PORT
value: "5432"
- name: ZITADEL_DATABASE_POSTGRES_USER_USERNAME
value: "zitadel"
- name: ZITADEL_DATABASE_POSTGRES_ADMIN_EXISTINGDATABASE
value: "zitadel"
- name: ZITADEL_DATABASE_POSTGRES_ADMIN_USERNAME
value: "zitadel"
- name: ZITADEL_DATABASE_POSTGRES_DATABASE
value: "zitadel"
- name: ZITADEL_DATABASE_POSTGRES_USER_SSL_MODE
value: "disable"
- name: ZITADEL_DATABASE_POSTGRES_ADMIN_SSL_MODE
value: "disable"
- name: ZITADEL_DEFAULTINSTANCE_ORG_HUMAN_USERNAME
value: "zitadel-admin"
- name: ZITADEL_DEFAULTINSTANCE_ORG_NAME
value: "Sarex"