diff --git a/infrastructure/security-audit/README.md b/infrastructure/security-audit/README.md new file mode 100644 index 0000000..56a101d --- /dev/null +++ b/infrastructure/security-audit/README.md @@ -0,0 +1,153 @@ +# security-audit + +Сбор журнала аудита kube-apiserver с мастеров в Kafka-топик `security.k8s-audit`. +Одно сообщение — одно событие аудита в JSON (`encoding: raw`, без OTLP-обёртки), обогащённое полями `sarex_*`. +Дальнейшие этапы (processor → MinIO/Parquet) — отдельными компонентами. + +С observability-коллектором (`infrastructure/opentelemetry-collector`) не связан: отдельный релиз, namespace и конфиг. + +| Оверлей | Статус | +|---|---| +| `uralkal` | манифесты готовы, **в `clusters/uralkal` не подключён** | + +## Что делает коллектор + +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 <]' + + 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`, 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`. + +## Проверка после подключения + +- 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. + +## Проверить перед подключением + +Сверено по репозиторию и чартам 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 | + +Остаётся открытым: + +- `TODO(verify)` **nodelocaldns** (`169.254.25.10`) в NetworkPolicy: в репозитории не подтверждается. При дефолте kubespray + включён, если нет — правило безвредно. +- Некритичные предупреждения 0.121.0: `service::telemetry::metrics::address` устарел (так же во всех коллекторах репо); + OTTL-пути без префикса `log.` (контекст выводится автоматически). diff --git a/infrastructure/security-audit/base/helmrelease-collector.yaml b/infrastructure/security-audit/base/helmrelease-collector.yaml new file mode 100644 index 0000000..061b85a --- /dev/null +++ b/infrastructure/security-audit/base/helmrelease-collector.yaml @@ -0,0 +1,242 @@ +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} + + 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 diff --git a/infrastructure/security-audit/base/kustomization.yaml b/infrastructure/security-audit/base/kustomization.yaml new file mode 100644 index 0000000..acf1053 --- /dev/null +++ b/infrastructure/security-audit/base/kustomization.yaml @@ -0,0 +1,7 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +namespace: security-audit +resources: + - namespace.yaml + - networkpolicy.yaml + - helmrelease-collector.yaml diff --git a/infrastructure/security-audit/base/namespace.yaml b/infrastructure/security-audit/base/namespace.yaml new file mode 100644 index 0000000..5fb139d --- /dev/null +++ b/infrastructure/security-audit/base/namespace.yaml @@ -0,0 +1,10 @@ +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: disabled diff --git a/infrastructure/security-audit/base/networkpolicy.yaml b/infrastructure/security-audit/base/networkpolicy.yaml new file mode 100644 index 0000000..53f0d64 --- /dev/null +++ b/infrastructure/security-audit/base/networkpolicy.yaml @@ -0,0 +1,32 @@ +# Коллектору нужны только DNS, Kafka и Vault (агент-инжектор). Входящие — только health-check kubelet'а. +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: audit-collector +spec: + podSelector: {} + policyTypes: [Ingress, Egress] + ingress: + - ports: + - {port: 13133, protocol: TCP} # health_check (liveness/readiness) + egress: + - to: + - namespaceSelector: + matchLabels: {kubernetes.io/metadata.name: kube-system} + podSelector: + matchLabels: {k8s-app: kube-dns} + - ipBlock: {cidr: 169.254.25.10/32} # nodelocaldns kubespray; TODO(verify): enable_nodelocaldns в контуре + ports: + - {port: 53, protocol: UDP} + - {port: 53, protocol: TCP} + - to: + - namespaceSelector: + matchLabels: {kubernetes.io/metadata.name: kafka} + ports: + # client-листенер SASL_SSL: bootstrap kafka-kafka-contour и advertised-адреса подов controller-headless + - {port: 9092, protocol: TCP} + - to: + - namespaceSelector: + matchLabels: {kubernetes.io/metadata.name: vault} + ports: + - {port: 8200, protocol: TCP} # vault-vault-contour.vault.svc:8200 (AGENT_INJECT_VAULT_ADDR) diff --git a/infrastructure/security-audit/kustomization.yaml b/infrastructure/security-audit/kustomization.yaml new file mode 100644 index 0000000..85dcd9d --- /dev/null +++ b/infrastructure/security-audit/kustomization.yaml @@ -0,0 +1,4 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - base diff --git a/infrastructure/security-audit/uralkal/audit-collector.yaml b/infrastructure/security-audit/uralkal/audit-collector.yaml new file mode 100644 index 0000000..c475ca8 --- /dev/null +++ b/infrastructure/security-audit/uralkal/audit-collector.yaml @@ -0,0 +1,18 @@ +apiVersion: helm.toolkit.fluxcd.io/v2 +kind: HelmRelease +metadata: + name: audit-collector + namespace: security-audit +spec: + dependsOn: + - name: kafka + namespace: kafka + - name: vault + namespace: vault + values: + extraEnvs: + - name: CONTOUR + value: uralkal + # Service bitnami-чарта kafka-contour (релиз kafka); тот же адрес у приложений apps/*/uralkal. + - name: KAFKA_BROKERS + value: kafka-kafka-contour.kafka.svc.cluster.local:9092 diff --git a/infrastructure/security-audit/uralkal/kustomization.yaml b/infrastructure/security-audit/uralkal/kustomization.yaml new file mode 100644 index 0000000..dabe7fd --- /dev/null +++ b/infrastructure/security-audit/uralkal/kustomization.yaml @@ -0,0 +1,6 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - ../base +patches: + - path: audit-collector.yaml