Модуль 4. Безопасность и соответствие
Цель — снизить риски утечек, несанкционированного доступа и внедрить управляемые практики безопасности в BI/DWH на k8s: изоляция, аутентификация/авторизация, защита секретов, контроль выхода в сеть, проверка образов, аудит и краткоживущие токены для коннекторов. Всё это должно быть «кодом» (policy-as-code), проходить через CI/CD и иметь наблюдаемость.
RBAC и Namespace-изоляция
Namespace как граница ответственности
- Делите среду на ns по контурам/командам: bi-prod, bi-stage, data-platform, mdm, catalog.
- Включайте default deny NetworkPolicy в каждом ns (см. раздел 3).
- Для секретов/регистр-credentials — отдельный ns platform-secrets с контролируемым доступом.
RBAC (Roles/ClusterRoles, Bindings)
- Принцип наименьших привилегий: роль на ns, а не кластер; запрет */patch/delete там, где не требуется.
- Разделите роли: viewer, deployer (apply только в своем ns), operator (restarts, logs).
Пример: «только деплой в своем ns»
apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: name: deployer namespace: bi rules: - apiGroups: ["apps"] resources: ["deployments","statefulsets","daemonsets","replicasets"] verbs: ["get","list","watch","create","update","patch"] - apiGroups: [""] resources: ["services","configmaps","secrets"] verbs: ["get","list","watch","create","update","patch"] --- apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: name: bind-deployer namespace: bi subjects: - kind: Group name: ldap:bi-devops # группа из IdP roleRef: kind: Role name: deployer apiGroup: rbac.authorization.k8s.io
Pod Security (PSA) и OPA Gatekeeper
Pod Security Admission (встроенный)
Метки на namespace включают базовые профили:
- pod-security.kubernetes.io/enforce: restricted
- pod-security.kubernetes.io/audit: restricted
- pod-security.kubernetes.io/warn: baseline
Это запрещает привилегированные контейнеры, hostPID/IPC/Network, требует runAsNonRoot, ограничивает capabilities, volume types и т. п.
kubectl label ns bi pod-security.kubernetes.io/enforce=restricted \ pod-security.kubernetes.io/warn=restricted \ pod-security.kubernetes.io/audit=restricted
OPA Gatekeeper (policy-as-code)
Используйте Gatekeeper для обязательных правил (Rego). Примеры практических ограничений:
- Только разрешённые реестры образов.
- Обязательные securityContext: runAsNonRoot: true, readOnlyRootFilesystem: true, allowPrivilegeEscalation: false, drop ALL.
- Запрет hostNetwork, hostPath.
- Обязательные resources.requests/limits.
Шаблон (ConstraintTemplate) «образ только из allowlist реестров»:
apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
name: k8sallowedregistries
spec:
crd:
spec:
names:
kind: K8sAllowedRegistries
validation:
openAPIV3Schema:
properties:
repos:
type: array
items: { type: string }
targets:
- target: admission.k8s.gatekeeper.sh
rego: |
package k8sallowedregistries
violation[{"msg": msg}] {
input.review.kind.kind == "Pod"
some c
image := input.review.object.spec.containers[c].image
allowed := {r | r := input.parameters.repos[_]}
not startswith_any(image, allowed)
msg := sprintf("image %v not from allowed registries %v", [image, allowed])
}
startswith_any(img, allowed) {
some a; startswith(img, allowed[a])
}
Ограничение (Constraint):
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sAllowedRegistries
metadata:
name: only-corp-registries
spec:
match:
kinds: [{ apiGroups: [""], kinds: ["Pod"] }]
parameters:
repos:
- "ghcr.io/your-org/"
- "registry.company.local/"
NetworkPolicy — изоляция трафика (ingress и egress)
Блок по умолчанию
Сначала запретить всё, потом разрешать адресно.
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-all
namespace: bi
spec:
podSelector: {}
policyTypes: ["Ingress","Egress"]
Разрешения по минимуму
- DNS к kube-dns.
- Доступ к БД-витринам (Postgres/ClickHouse/Trino) по сервисам/лейблам.
- Доступ к объектному хранилищу (MinIO/S3).
- Внешний OIDC/SMTP — только из подов, где это нужно.
Пример egress-политики для Metabase/Superset:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-egress-needed
namespace: bi
spec:
podSelector:
matchLabels:
app: metabase
policyTypes: ["Egress"]
egress:
# DNS
- to:
- namespaceSelector: { matchLabels: { kubernetes.io/metadata.name: kube-system } }
podSelector: { matchLabels: { k8s-app: kube-dns } }
ports: [{ protocol: UDP, port: 53 }]
# Postgres в ns data-platform
- to:
- namespaceSelector: { matchLabels: { name: data-platform } }
podSelector: { matchLabels: { app: postgres } }
ports: [{ protocol: TCP, port: 5432 }]
# MinIO (S3 API)
- to:
- namespaceSelector: { matchLabels: { name: data-platform } }
podSelector: { matchLabels: { app: minio } }
ports:
- { protocol: TCP, port: 9000 }
- { protocol: TCP, port: 9001 }
Secret-менеджмент: External Secrets + Vault (или SOPS/SealedSecrets)
Проблема «секретов в yaml»
Обычные Secret — это Base64, не шифрование. Храним секреты вне кластера (Vault/Cloud Secret Manager), а в k8s — только синхронизацию.
External Secrets Operator (ESO)
Синхронизирует секреты из внешнего хранилища в Secret.
Пример: тянем динамические креды из HashiCorp Vault
apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
name: vault-store
namespace: bi
spec:
provider:
vault:
server: "http://vault.platform:8200"
path: "kv"
version: "v2"
auth:
kubernetes:
mountPath: "auth/kubernetes"
role: "bi-apps"
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: metabase-db-credentials
namespace: bi
spec:
refreshInterval: 1h
secretStoreRef:
name: vault-store
kind: SecretStore
target:
name: metabase-db-secret
creationPolicy: Owner
data:
- secretKey: username
remoteRef: { key: "db/bi/metabase", property: "username" }
- secretKey: password
remoteRef: { key: "db/bi/metabase", property: "password" }
Динамические/краткоживущие (плавающие) токены
- Vault Database Engine: выдаёт временные PostgreSQL-пользователи с TTL (минуты/часы) и auto-revoke — идеально для коннекторов ETL/BI.
- STS-подобные токены для облаков/хранилищ — выдача краткоживущих ключей.
- Projected service account tokens: токены k8s c ограниченной аудиторией/TTL.
ServiceAccount с отключённым авто-монтажом и явным projected токеном:
apiVersion: v1
kind: ServiceAccount
metadata:
name: bi-app
namespace: bi
automountServiceAccountToken: false
---
apiVersion: v1
kind: Pod
metadata:
name: app-with-projected-token
namespace: bi
spec:
serviceAccountName: bi-app
automountServiceAccountToken: false
volumes:
- name: k8s-token
projected:
sources:
- serviceAccountToken:
audience: "vault"
expirationSeconds: 1800
path: token
containers:
- name: app
image: ghcr.io/your-org/app:1.0
volumeMounts:
- name: k8s-token
mountPath: /var/run/secrets/tokens
readOnly: true
SSO: OIDC/SAML для BI (на краю или нативно)
Два подхода
- «На краю» через Ingress + oauth2-proxy — быстрая интеграция для любых веб-приложений.
- Нативная интеграция в приложении (Metabase/Superset поддерживают OIDC напрямую) — тонкая настройка ролей/групп в самом BI.
Плюсы «на краю»: единообразие, централизованные политики, простые аннотации.
Минусы: маппинг ролей — через заголовки, меньше гибкости.
Сигнатурные образы, политика и сканирование
- Подписывайте образы с Sigstore Cosign; храните подписи в реестре.
-
Проверка на входе:
- Kyverno (проще) или Policy Controller (cosigned) для валидации подписи/аттестации.
- Gatekeeper хорош для реестров/секьюр-контекстов; для подписи практичнее Kyverno/cosign.
- Сканирование уязвимостей: Trivy/Grype, формируйте SBOM (Syft), ставьте quality-gates в CI.
Пример Kyverno (валидация cosign-подписи):
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: verify-image-signatures
spec:
validationFailureAction: enforce
rules:
- name: check-sig
match:
any:
- resources:
kinds: ["Pod"]
verifyImages:
- imageReferences:
- "ghcr.io/your-org/*"
attestors:
- entries:
- keys:
publicKeys: |-
-----BEGIN PUBLIC KEY-----
...
-----END PUBLIC KEY-----
Аудит, логи и SIEM
Kubernetes Audit Policy
На уровне API-сервера включите аудит и определите критичность событий: доступ к secrets, pods/exec, изменение RBAC, Gatekeeper/Kyverno нарушения.
Пример политики (фрагмент):
apiVersion: audit.k8s.io/v1
kind: Policy
rules:
- level: Metadata
resources:
- group: ""
resources: ["pods","secrets","configmaps"]
- level: RequestResponse
verbs: ["update","patch","delete","create"]
resources:
- group: "rbac.authorization.k8s.io"
resources: ["rolebindings","clusterrolebindings","roles","clusterroles"]
- level: Request
nonResourceURLs: ["/api*", "/version"]
verbs: ["get"]
Доставляйте аудит-логи в Loki/EFK и/или SIEM, настройте алерты на чувствительные события.
Runtime-мониторинг
- Falco (eBPF) — детект выполнения шелла в контейнере, изменения бинарей, доступ к sensitive-файлам. Полезен для «живого» уровня.
Типовые риски и как их закрыть
-
Случайная публикация сервиса наружу.
Решение: разделяйте ingressClass (external/internal), запрет внешнего LB по умолчанию, Git-проверки доменов/аннотаций. -
Отсутствие сетевых политик → lateral movement.
Решение: default-deny, whitelist на DNS/БД/S3/IdP, шаблоны полисей для команд. -
Привилегированные контейнеры/hostPath/hostNetwork.
Решение: Pod Security restricted + Gatekeeper правила на запрет. -
Секреты в репозиториях/ConfigMap.
Решение: ESO + Vault/SOPS, аудит CI, pre-commit hooks (git-secrets), ротации. -
Долгоживущие токены/ключи.
Решение: динамические креды Vault, projected tokens с TTL, короткие срокики клиента OIDC (refresh через прокси). -
Не подписанные/уязвимые образы.
Решение: cosign + Kyverno/cosigned, Trivy в CI, allowlist реестров Gatekeeper. -
Нет аудита и алертов.
Решение: audit-policy + доставка в SIEM/лог-хранилище, алерты на RBAC/exec/секреты.
ПРАКТИКА
Практика A. Подключить BI к корпоративному SSO через Ingress + oauth2-proxy
Предположим, у вас IdP (Keycloak/AD/Okta) с OIDC. Схема: пользователь → Ingress (nginx) → oauth2-proxy (OIDC) → Metabase/Superset. Группы приходят в заголовках, BI маппит роли.
- Секрет с ClientID/Secret для oauth2-proxy
apiVersion: v1 kind: Secret metadata: name: oauth2-proxy-secret namespace: bi type: Opaque stringData: client-id: "bi-oidc" client-secret: "REDACTED" cookie-secret: "base64-32-bytes" # `openssl rand -base64 32`
- Deployment oauth2-proxy
apiVersion: apps/v1
kind: Deployment
metadata:
name: oauth2-proxy
namespace: bi
spec:
replicas: 2
selector: { matchLabels: { app: oauth2-proxy } }
template:
metadata: { labels: { app: oauth2-proxy } }
spec:
containers:
- name: oauth2-proxy
image: quay.io/oauth2-proxy/oauth2-proxy:v7.6.0
args:
- --provider=oidc
- --oidc-issuer-url=https://idp.company.local/realms/PROD
- --client-id=$(CLIENT_ID)
- --client-secret=$(CLIENT_SECRET)
- --cookie-secret=$(COOKIE_SECRET)
- --cookie-secure=true
- --http-address=0.0.0.0:4180
- --email-domain=*
- --pass-authorization-header=true
- --set-authorization-header=true
- --set-xauthrequest=true
- --scope=openid profile email groups
- --whitelist-domain=.company.local
env:
- name: CLIENT_ID
valueFrom: { secretKeyRef: { name: oauth2-proxy-secret, key: client-id } }
- name: CLIENT_SECRET
valueFrom: { secretKeyRef: { name: oauth2-proxy-secret, key: client-secret } }
- name: COOKIE_SECRET
valueFrom: { secretKeyRef: { name: oauth2-proxy-secret, key: cookie-secret } }
ports: [{ containerPort: 4180 }]
---
apiVersion: v1
kind: Service
metadata:
name: oauth2-proxy
namespace: bi
spec:
selector: { app: oauth2-proxy }
ports: [{ port: 80, targetPort: 4180 }]
- Ingress для Metabase с аутентификацией через oauth2-proxy
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: metabase-ing
namespace: bi
annotations:
kubernetes.io/ingress.class: nginx
nginx.ingress.kubernetes.io/auth-url: "http://oauth2-proxy.bi.svc.cluster.local/oauth2/auth"
nginx.ingress.kubernetes.io/auth-signin: "https://metabase.company.local/oauth2/start?rd=$scheme://$host$request_uri"
nginx.ingress.kubernetes.io/configuration-snippet: |
auth_request_set $user $upstream_http_x_auth_request_user;
auth_request_set $email $upstream_http_x_auth_request_email;
auth_request_set $groups $upstream_http_x_auth_request_groups;
proxy_set_header X-User $user;
proxy_set_header X-Email $email;
proxy_set_header X-Groups $groups;
spec:
tls:
- hosts: ["metabase.company.local"]
secretName: bi-tls
rules:
- host: metabase.company.local
http:
paths:
- path: /
pathType: Prefix
backend: { service: { name: metabase, port: { number: 80 } } }
Аналогично для Superset. В BI настроить SSO-мэппинг групп (передаются в X-Groups) на роли.
-
Группы и доступ
В IdP (Keycloak) у пользователя группы bi-viewers, bi-admins. На уровне BI маппим: bi-viewers → Viewer, bi-admins → Admin. -
Алёрты и логи
Включите логи доступа Ingress + логи oauth2-proxy, заведите алерты на рост 401/403 и истечение TLS.
Практика B. Запрет egress по умолчанию
- Default deny для ns bi (ingress+egress)
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny
namespace: bi
spec:
podSelector: {}
policyTypes: ["Ingress","Egress"]- Разрешить egress только к DNS, Postgres, MinIO и IdP
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-essential-egress
namespace: bi
spec:
podSelector: {}
policyTypes: ["Egress"]
egress:
- to:
- namespaceSelector: { matchLabels: { kubernetes.io/metadata.name: kube-system } }
podSelector: { matchLabels: { k8s-app: kube-dns } }
ports: [{ protocol: UDP, port: 53 }]
- to:
- namespaceSelector: { matchLabels: { name: data-platform } }
podSelector: { matchLabels: { app: postgres } }
ports: [{ protocol: TCP, port: 5432 }]
- to:
- namespaceSelector: { matchLabels: { name: data-platform } }
podSelector: { matchLabels: { app: minio } }
ports:
- { protocol: TCP, port: 9000 }
- { protocol: TCP, port: 9001 }
- to:
- ipBlock: { cidr: 10.10.10.0/24 } # ваш IdP/Keycloak (пример)
ports: [{ protocol: TCP, port: 443 }]- Тестирование
- Попробуйте из Pod выполнить curl на внешний интернет — должно быть запрещено.
- Проверьте доступ к БД и MinIO — должен работать.
- Включите метрики/логи NetworkPolicy (Calico/Cilium flow logs) — пригодится для отладки.
Чек-лист «минимум для продакшна»
- Namespaces разграничены, PSA restricted включён.
- RBAC по группам IdP, нет лишних кластерных прав.
- NetworkPolicy: default-deny + адресные разрешения (DNS/БД/S3/IdP/SMTP).
- Секреты через ESO + Vault (или SOPS/SealedSecrets), ротируются.
- Краткоживущие токены: Vault dynamic creds, projected SA tokens с TTL.
- Ingress: TLS modern, SSO OIDC, WAF (по возможности), лимиты/таймауты.
- Подписи образов (cosign) и валидация (Kyverno/cosigned), сканер уязвимостей (Trivy).
- Audit policy включён, доставка логов в SIEM, алерты на RBAC/exec/секреты.
- Runbooks: инциденты доступа, ротация секретов, компрометация токена.
- Политики Gatekeeper/Kyverno — в репозитории, через GitOps.
Безопасность в k8s для BI/DWH — это не «одна настройка», а связка: изоляция + аутентификация/SSO + секреты вне кластера + политика образов + аудит + сетевые правила по умолчанию. Внедряйте через policy-as-code и GitOps, используйте краткоживущие токены и динамические креды, а вывод наружу — только через защищённый Ingress с SSO и WAF.



