Skip to content

Security & Hardening

Production-grade security posture for the BANA Kubernetes cluster — covering pod-level restrictions, namespace isolation, RBAC, image supply chain, and scheduling priorities.

Pod Security Standards (PSS)

Kubernetes Pod Security Standards define three profiles: privileged, baseline, and restricted. BANA enforces these at the namespace level via labels.

Namespace Labels

Production namespaces — restricted profile
yaml
# Production namespaces — restricted profile
apiVersion: v1
kind: Namespace
metadata:
  name: nx-backend
  labels:
    app.kubernetes.io/part-of: bana
    pod-security.kubernetes.io/enforce: restricted
    pod-security.kubernetes.io/enforce-version: latest
    pod-security.kubernetes.io/audit: restricted
    pod-security.kubernetes.io/warn: restricted
---
apiVersion: v1
kind: Namespace
metadata:
  name: nx-app
  labels:
    app.kubernetes.io/part-of: bana
    pod-security.kubernetes.io/enforce: restricted
    pod-security.kubernetes.io/enforce-version: latest
    pod-security.kubernetes.io/audit: restricted
    pod-security.kubernetes.io/warn: restricted
Staging namespaces — also use restricted:latest enforce mode
yaml
# Staging namespaces — also use restricted:latest enforce mode
# (matches production — all namespaces including nx-broker use restricted)
apiVersion: v1
kind: Namespace
metadata:
  name: nx-backend
  labels:
    app.kubernetes.io/part-of: bana
    pod-security.kubernetes.io/enforce: restricted
    pod-security.kubernetes.io/warn: restricted

INFO

Staging uses the same restricted enforce mode as production across all namespaces (including nx-broker). This ensures security parity and catches PSS violations early.

Profile Summary

ProfileNamespacesUsage
restrictednx-backend, nx-app, nx-persistent, nx-broker, nx-search, nx-internal, nx-watcher (both staging and production)All namespaces enforce restricted profile

SecurityContext Template

Every container in BANA must run with a hardened SecurityContext. This template is the baseline for all backend and frontend Deployments.

The Dockerfile uses the built-in bun user (UID 1000) from the oven/bun:*-alpine base image via USER bun. The K8s SecurityContext matches this with runAsUser: 1000 / runAsGroup: 1000.

Pod-Level SecurityContext

### Pod-Level SecurityContext
yaml
spec:
  template:
    spec:
      securityContext:
        runAsNonRoot: true
        runAsUser: 1000   # matches 'bun' user in Dockerfile
        runAsGroup: 1000  # matches 'bun' group in Dockerfile
        fsGroup: 1000
        seccompProfile:
          type: RuntimeDefault

Container-Level SecurityContext

### Container-Level SecurityContext
yaml
containers:
  - name: <service>
    securityContext:
      allowPrivilegeEscalation: false
      readOnlyRootFilesystem: true
      capabilities:
        drop:
          - ALL

Writable Directories

Since readOnlyRootFilesystem: true prevents writes to the container filesystem, services that need writable directories use emptyDir volumes:

Since readOnlyRootFilesystem: true prevents writes to the ...
yaml
containers:
  - name: <service>
    volumeMounts:
      - name: tmp
        mountPath: /tmp
volumes:
  - name: tmp
    emptyDir:
      sizeLimit: 64Mi

TIP

Bun and Node.js may write to /tmp for caching. Always mount an emptyDir at /tmp when using readOnlyRootFilesystem: true.

Complete Hardened Deployment Example

Deployment: nx-commerce
yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nx-commerce
  namespace: nx-backend
spec:
  replicas: 1
  selector:
    matchLabels:
      app.kubernetes.io/name: commerce
  template:
    metadata:
      labels:
        app.kubernetes.io/name: commerce
    spec:
      automountServiceAccountToken: false
      securityContext:
        runAsNonRoot: true
        runAsUser: 1000
        runAsGroup: 1000
        fsGroup: 1000
        seccompProfile:
          type: RuntimeDefault
      containers:
        - name: commerce
          image: bcr.bana.com.vn/nx-commerce:0.0.1
          securityContext:
            allowPrivilegeEscalation: false
            readOnlyRootFilesystem: true
            capabilities:
              drop:
                - ALL
          ports:
            - containerPort: 3000
          volumeMounts:
            - name: tmp
              mountPath: /tmp
      volumes:
        - name: tmp
          emptyDir:
            sizeLimit: 64Mi

ServiceAccount Hardening

By default, Kubernetes mounts a ServiceAccount token into every pod. Most application pods don't need to call the Kubernetes API, so this token is an unnecessary attack surface.

Disable Auto-Mount

yaml
spec:
  template:
    spec:
      automountServiceAccountToken: false

Apply this to all backend and frontend Deployments.

Dedicated ServiceAccounts

Services that need Kubernetes API access get their own ServiceAccount with minimal permissions:

ServiceAccount: nx-traefik
yaml
apiVersion: v1
kind: ServiceAccount
metadata:
  name: nx-traefik
  namespace: nx-internal
  labels:
    app.kubernetes.io/name: traefik
    app.kubernetes.io/part-of: bana
automountServiceAccountToken: false  # Traefik uses file provider, no K8s API access needed

TIP

Traefik in BANA uses a file provider configuration, not Kubernetes CRD discovery. This means it does not need to watch traefik.io CRDs and requires minimal or no Kubernetes API access, keeping the RBAC footprint small.

ServiceAccountNamespacePurposeAPI Access
defaultAllApp podsDisabled (automountServiceAccountToken: false)
nx-traefiknx-internalTraefik file providerMinimal (no CRD watching needed)
nx-cert-managercert-managercert-managerSecrets, Ingress, CRDs
nx-prometheusnx-watcherPrometheus scrapingRead-only pods, services, endpoints

RBAC

GitLab CI/CD Service Account

The CI/CD pipeline needs kubectl apply access scoped to specific namespaces — never cluster-admin.

ServiceAccount: gitlab-deployer
yaml
apiVersion: v1
kind: ServiceAccount
metadata:
  name: gitlab-deployer
  namespace: nx-backend
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: gitlab-deployer
  namespace: nx-backend
rules:
  - apiGroups: ["", "apps", "batch", "autoscaling", "policy"]
    resources:
      - deployments
      - services
      - configmaps
      - secrets
      - jobs
      - horizontalpodautoscalers
      - poddisruptionbudgets
    verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
  - apiGroups: ["traefik.io"]
    resources: ["ingressroutes", "middlewares"]
    verbs: ["get", "list", "watch", "create", "update", "patch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: gitlab-deployer
  namespace: nx-backend
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: Role
  name: gitlab-deployer
subjects:
  - kind: ServiceAccount
    name: gitlab-deployer
    namespace: nx-backend

Repeat for each namespace the pipeline deploys to (nx-app, nx-backend).

Portainer Read-Only Role

ClusterRole: nx-portainer-readonly
yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: nx-portainer-readonly
rules:
  - apiGroups: ["", "apps", "batch", "autoscaling", "policy", "networking.k8s.io"]
    resources: ["*"]
    verbs: ["get", "list", "watch"]
  - apiGroups: [""]
    resources: ["pods/log"]
    verbs: ["get"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: nx-portainer-readonly
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: nx-portainer-readonly
subjects:
  - kind: ServiceAccount
    name: portainer-readonly
    namespace: portainer

RBAC Summary

PrincipalScopePermissionsPurpose
gitlab-deployerPer namespaceFull CRUD on deployments, services, configs, jobsCI/CD pipeline
nx-portainer-readonlyCluster-wideRead-only all resources + pod logsPortainer dashboard for PM/QA
nx-traefiknx-internalMinimal (file provider, no CRD watching)API gateway routing
nx-prometheusCluster-wideRead-only pods, services, endpointsMetrics scraping

Image Supply Chain

Trivy Scanning in CI

Every image is scanned for vulnerabilities before being pushed to the registry. See Operations for the full .gitlab-ci.yml integration.

Added to .gitlab-ci.yml after docker build, before push
yaml
# Added to .gitlab-ci.yml after docker build, before push
scan-image:
  stage: scan
  image:
    name: aquasec/trivy:latest
    entrypoint: [""]
  script:
    - trivy image --exit-code 1 --severity CRITICAL,HIGH --no-progress $REGISTRY/$SERVICE:$CI_COMMIT_SHORT_SHA
  allow_failure: false

Image Signing (cosign)

Sign images after push to ensure integrity:

bash
# In CI after push
cosign sign --key env://COSIGN_PRIVATE_KEY $REGISTRY/$SERVICE:$CI_COMMIT_SHORT_SHA

Admission Control

Use an admission controller (e.g., Kyverno) to block unscanned or unsigned images in production:

ClusterPolicy: require-image-scan
yaml
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: require-image-scan
spec:
  validationFailureAction: Enforce
  background: true
  rules:
    - name: check-image-registry
      match:
        any:
          - resources:
              kinds:
                - Pod
              namespaces:
                - nx-backend
                - nx-app
      validate:
        message: "Images must come from the bcr.bana.com.vn registry"
        pattern:
          spec:
            containers:
              - image: "bcr.bana.com.vn/nx-*"
    - name: block-latest-tag
      match:
        any:
          - resources:
              kinds:
                - Pod
              namespaces:
                - nx-backend
      validate:
        message: "Production images must use a specific tag, not :latest"
        pattern:
          spec:
            containers:
              - image: "!*:latest"

Image Pull Policy

EnvironmentPolicyRationale
StagingIfNotPresentFaster restarts, acceptable staleness
ProductionAlwaysEnsures the exact tag content matches the registry

PriorityClass

PriorityClasses ensure critical services survive node pressure and OOM events. Lower-priority pods are preempted first.

Definitions

PriorityClass: nx-system-critical
yaml
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
  name: nx-system-critical
value: 1000000
globalDefault: false
description: "Data layer, ingress controller"
---
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
  name: nx-high
value: 500000
globalDefault: false
description: "Identity (JWKS), API gateway"
---
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
  name: nx-default
value: 100000
globalDefault: true
description: "Backend services & frontends"
---
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
  name: nx-low
value: 10000
globalDefault: false
description: "Background workers, batch jobs"

Assignment

PriorityClassValueServices
nx-system-critical1,000,000nginx-ingress, cert-manager, data layer StatefulSets
nx-high500,000identity, Traefik
nx-default100,000commerce, finance, inventory, ledger, pricing, sale, signal, payment-api, client, bo, overture, sale-renderer
nx-low10,000payment-worker, wiki

Usage in Deployment

yaml
spec:
  template:
    spec:
      priorityClassName: nx-high  # identity, Traefik

WARNING

Never set preemptionPolicy: Never on high-priority classes. The whole point of PriorityClass is that critical services can preempt non-critical ones during resource contention.

LimitRange

Each namespace has a LimitRange that enforces minimum resource requests per container, preventing pods from being scheduled with dangerously low resources.

NamespaceMin CPUMin MemoryDefault CPUDefault Memory
nx-backend50m64Mi500m512Mi
nx-app25m32Mi200m256Mi
nx-broker100m128Mi500m768Mi
nx-persistent100m256Mi500m1Gi
nx-search50m128Mi200m512Mi
nx-internal50m64Mi500m512Mi
nx-watcher25m32Mi200m256Mi

TIP

The nx-broker namespace enforces a minimum of cpu 100m and memory 128Mi per container. All namespaces have both minimum and maximum bounds to prevent resource abuse.

Checklist

Use this checklist when deploying a new service or auditing an existing one:

  • [ ] Pod SecurityContext: runAsNonRoot, runAsUser: 1000, seccompProfile: RuntimeDefault
  • [ ] Container SecurityContext: allowPrivilegeEscalation: false, readOnlyRootFilesystem: true, capabilities.drop: [ALL]
  • [ ] automountServiceAccountToken: false (unless API access needed)
  • [ ] Dedicated ServiceAccount if API access is required
  • [ ] Image from bcr.bana.com.vn/nx-* only
  • [ ] Trivy scan passing (no CRITICAL/HIGH vulnerabilities)
  • [ ] PriorityClass assigned
  • [ ] Namespace has correct PSS labels
  • [ ] No :latest tag in production manifests

Proprietary and Confidential. Unauthorized copying, distribution, or use of this software is strictly prohibited.