Deploying Elastic Service Fabric in Kubernetes

This chart deploys the Fortify Elastic Service Fabric (ESF) 26.2 control plane: the Scanner Job Controller (SJC) and the SJC operator. It expects external PostgreSQL and S3-compatible communal storage and exposes the SJC API over HTTPS.

Table of contents

Kubernetes versions

The repository production guide currently documents these tested Kubernetes versions:

Helm version

Tool prerequisites

The examples below use the shell variable ESF_NAMESPACE for the target Kubernetes namespace. Set this variable once before running any command and all examples will use it automatically.

Installation

The Fortify ESF chart installs the SJC deployment, the SJC operator, the custom metrics APIService, and the ProcessQueue CRD.

Installation prerequisites

Installation steps

  1. Set the OCI chart reference
  2. Set the deployment namespace
  3. Create the required secrets
  4. Create the production values file
  5. Install the chart
  6. Verify the installation

Set the OCI chart reference

Replace <chart-version> with the published chart version you want to deploy from Docker Hub OCI.

helm show chart oci://registry-1.docker.io/fortifydocker/helm-esf --version <chart-version>

If your environment requires authenticated Docker Hub pulls or you need to avoid anonymous pull limits, run helm registry login registry-1.docker.io before the commands below.

Set the deployment namespace

Set ESF_NAMESPACE to the Kubernetes namespace where you want to deploy. Replace default with your target namespace if you are not deploying to the default namespace. All subsequent kubectl and helm commands use this variable.

export ESF_NAMESPACE=default

Create the required secrets

Secret overview

Create the required secrets in the $ESF_NAMESPACE namespace using the names below. See instructions below for generating the TLS material and API token, and for the expected secret contents.

Purpose Secret name Required Keys Configurable
SJC API TLS fortify-sjc-secrets Always ca.crt, tls.crt, tls.key No. The chart hardcodes this name.
SJC API bearer token esf-sjc-credentials by default global.sjcApiAuthMode=k8s-secret (default) token Yes, via global.sjcApiAuthCred.
Database init credentials (user with admin privileges) esf-db-init-credentials by default Always username, password Yes, via sjc.databaseAuth.initAuthSecret.
Database app credentials esf-db-credentials by default Optional to precreate username, password Yes, via sjc.databaseAuth.authSecret.
S3 credentials esf-s3-credentials by default Always accessKeyId, secretAccessKey, sessionToken Yes, via sjc.csAuth.authCred.
Communal storage certificate esf-cs-cert by default sjc.csAuth.certificateSecret.enabled=true for self signed certificates tls.crt or the configured key Yes, via sjc.csAuth.certificateSecret.*.

Create the SJC TLS secret

The chart always mounts the SJC API certificate from the secret fortify-sjc-secrets. The certificate must be valid for the service name sjc-svc in the $ESF_NAMESPACE namespace.

Generate a local CA and sign an SJC certificate:

openssl genrsa -out ca.key 4096
openssl req -x509 -new -nodes -key ca.key -sha256 -days 365 \
  -out ca.crt \
  -subj "/C=US/ST=State/L=City/O=Organization/CN=${ESF_NAMESPACE}-local-ca"

openssl genrsa -out sjc.key 2048

cat > sjc-cert.conf <<EOF
[req]
distinguished_name = req_distinguished_name
req_extensions = v3_req
prompt = no

[req_distinguished_name]
C = US
ST = State
L = City
O = Organization
CN = sjc-svc.${ESF_NAMESPACE}.svc.cluster.local

[v3_req]
subjectAltName = @alt_names

[alt_names]
DNS.1 = sjc-svc
DNS.2 = sjc-svc.${ESF_NAMESPACE}
DNS.3 = sjc-svc.${ESF_NAMESPACE}.svc
DNS.4 = sjc-svc.${ESF_NAMESPACE}.svc.cluster.local
DNS.5 = localhost
IP.1 = 127.0.0.1
EOF

openssl req -new -key sjc.key -out sjc.csr -config sjc-cert.conf
openssl x509 -req -in sjc.csr -CA ca.crt -CAkey ca.key -CAcreateserial \
  -out sjc.crt -days 365 -sha256 -extensions v3_req -extfile sjc-cert.conf

Create the Kubernetes secret:

kubectl create secret generic fortify-sjc-secrets \
  --namespace "$ESF_NAMESPACE" \
  --from-file=ca.crt=ca.crt \
  --from-file=tls.crt=sjc.crt \
  --from-file=tls.key=sjc.key

For production CA-signed certificates, use the same SAN set for the target namespace deployment.

Create the API token secret

API_TOKEN=$(openssl rand -hex 32)

kubectl create secret generic esf-sjc-credentials \
  --namespace "$ESF_NAMESPACE" \
  --from-literal=token="$API_TOKEN"

If you use a different secret name, set global.sjcApiAuthCred to match it.

Create the database init secret

The sjc-init container uses the init secret to create the SJC schema, apply migrations, and create or rotate the application-level database secret.

kubectl create secret generic esf-db-init-credentials \
  --namespace "$ESF_NAMESPACE" \
  --from-literal=username='<db-admin-user>' \
  --from-literal=password='<db-admin-password>'

The application secret referenced by sjc.databaseAuth.authSecret defaults to esf-db-credentials. You can precreate it if required by your database policy:

kubectl create secret generic esf-db-credentials \
  --namespace "$ESF_NAMESPACE" \
  --from-literal=username='sjc_user' \
  --from-literal=password='<app-db-password>'

If the application secret is missing or invalid, sjc-init creates or rotates it during startup.

Create communal storage credentials

This deployment supports only sjc.csAuth.authMode=k8s-secret. Create the secret referenced by sjc.csAuth.authCred, and keep sjc.csAuth.useSSL=true.

kubectl create secret generic esf-s3-credentials \
  --namespace "$ESF_NAMESPACE" \
  --from-literal=accessKeyId='<access-key-id>' \
  --from-literal=secretAccessKey='<secret-access-key>' \
  --from-literal=sessionToken=''

Create the communal storage certificate secret (optional)

If the communal storage endpoint uses a self-signed or private CA certificate, add that certificate to a separate secret and enable sjc.csAuth.certificateSecret.

kubectl create secret generic esf-cs-cert \
  --namespace "$ESF_NAMESPACE" \
  --from-file=tls.crt='<path-to-storage-ca-or-server-cert>'

Create the production values file

Create a minimal override file and update the hostnames, paths, and authentication mode for your environment.

The rootUri value must follow the format s3://<endpoint>/<bucket>, where the S3 endpoint host comes first and the bucket name is the first path segment after the host. The example below uses the AWS regional endpoint for us-west-2; replace it with your actual S3-compatible endpoint, bucket name, and path prefix.

global:
  sjcApiAuthMode: k8s-secret
  sjcApiAuthCred: esf-sjc-credentials

sjc:
  databaseAuth:
    initAuthSecret: esf-db-init-credentials
    authSecret: esf-db-credentials
    host: postgres.example.internal
    database: sjc
    sslMode: require

  csAuth:
    authMode: k8s-secret
    authCred: esf-s3-credentials
    rootUri: s3://s3.us-west-2.amazonaws.com/my-esf-bucket
    region: us-west-2
    useSSL: true
    skipTLSVerify: false
    # certificateSecret:
    #   enabled: true
    #   name: esf-cs-cert
    #   key: tls.crt

Set sjc.csAuth.authCred to the name of the communal storage credentials secret you created and keep sjc.csAuth.useSSL=true.

The supported configuration reference is documented in the Values section below.

Install the chart

helm upgrade --install fortify-esf oci://registry-1.docker.io/fortifydocker/helm-esf \
  --version <chart-version> \
  --namespace "$ESF_NAMESPACE" \
  --create-namespace \
  --values values-production.yaml

Verify the installation

Check the Helm release and workloads:

helm status fortify-esf --namespace "$ESF_NAMESPACE"
kubectl get pods -n "$ESF_NAMESPACE"
kubectl get svc sjc-svc -n "$ESF_NAMESPACE"
kubectl get crd processqueues.esf.fortify.com
kubectl get apiservice v1beta2.custom.metrics.k8s.io

Check logs for the controller and operator:

kubectl logs deployment/sjc -c sjc -n "$ESF_NAMESPACE" --tail=50
kubectl logs deployment/sjc-operator-controller-manager -n "$ESF_NAMESPACE" --tail=50

Verify that the HTTPS endpoint responds:

kubectl port-forward svc/sjc-svc 8443:31031 -n "$ESF_NAMESPACE"
curl -k https://localhost:8443/health

The /health endpoint does not require a bearer token.

Upgrading

  1. Back up the currently applied values.
  2. Set the new OCI chart version.
  3. Review the new chart defaults and update your override file.
  4. Run helm upgrade.
  5. Re-verify the pods, CRD, APIService, and logs.

Before running any upgrade command, ensure ESF_NAMESPACE is set to the namespace of your existing release:

ESF_NAMESPACE=default  # replace with your actual release namespace

Preparing for the upgrade

helm get values fortify-esf --namespace "$ESF_NAMESPACE" -o yaml > current-values.yaml
helm show values oci://registry-1.docker.io/fortifydocker/helm-esf --version <chart-version> > new-values.yaml

Review new-values.yaml from the OCI chart and merge any new keys into your environment-specific override file.

Performing the upgrade

helm upgrade fortify-esf oci://registry-1.docker.io/fortifydocker/helm-esf \
  --version <chart-version> \
  --namespace "$ESF_NAMESPACE" \
  --values values-production.yaml

Verifying the upgrade

helm status fortify-esf --namespace "$ESF_NAMESPACE"
kubectl get pods -n "$ESF_NAMESPACE"
kubectl get crd processqueues.esf.fortify.com
kubectl logs deployment/sjc -c sjc -n "$ESF_NAMESPACE" --tail=50
kubectl logs deployment/sjc-operator-controller-manager -n "$ESF_NAMESPACE" --tail=50

Troubleshooting

SJC does not start because fortify-sjc-secrets is missing or malformed

The main deployment hardcodes the secret name fortify-sjc-secrets. Verify the secret exists in the release namespace and contains ca.crt, tls.crt, and tls.key.

kubectl get secret fortify-sjc-secrets -n "$ESF_NAMESPACE" -o yaml

API requests return HTTP 401

If global.sjcApiAuthMode is k8s-secret, verify that the bearer token secret named by global.sjcApiAuthCred exists and that your client is sending Authorization: Bearer <token>.

sjc-init fails or the app DB secret rotates unexpectedly

The init container must be able to read the init secret and create or update the application secret named by sjc.databaseAuth.authSecret. Confirm that the init credentials have sufficient database permissions and that the Kubernetes secret is writable by the pod's service account.

Communal storage authentication fails

Make sure sjc.csAuth.authMode is k8s-secret, sjc.csAuth.authCred points to the secret you created, and that secret contains accessKeyId, secretAccessKey, and sessionToken.

Communal storage TLS validation fails

If the storage endpoint uses a self-signed or private CA certificate, create the certificate secret and enable sjc.csAuth.certificateSecret.enabled=true. Do not set sjc.csAuth.skipTLSVerify=true in production unless you fully understand the trust implications.

The operator or CRD is missing after installation

Verify that fortify-esf-operator.crd.enable=true and fortify-esf-operator.rbac.enable=true, then check the operator deployment logs.

kubectl get crd processqueues.esf.fortify.com
kubectl logs deployment/sjc-operator-controller-manager -n "$ESF_NAMESPACE" --tail=50

Helm refuses to take ownership of the CRD if installed previously in another namespace.

The processqueues.esf.fortify.com CRD is cluster-scoped (not namespaced), and a stale copy from the previous installation in another namespace would be still present in the cluster if fortify-esf-operator.crd.keep in the values set to true. It has Helm annotations pointing to <previous_namespace>/fortify-esf, so when installing the ESF chart in new namespace, Helm refuses to take ownership of the CRD — "resource already exists and is not managed by Helm." You can patch CRD annotations to point to the new release:

kubectl annotate crd processqueues.esf.fortify.com \
  meta.helm.sh/release-name=<esf release name> \
  meta.helm.sh/release-namespace=<new namespace> \
  --overwrite
kubectl label crd processqueues.esf.fortify.com \
  app.kubernetes.io/managed-by=Helm --overwrite

Values

Key Type Default Description
customResources list [] Extra Kubernetes resources to deploy alongside the chart. Each entry is rendered verbatim.
fortify-esf-operator.certmanager.enable bool false
fortify-esf-operator.controllerManager.container.args[0] string "--leader-elect"
fortify-esf-operator.controllerManager.container.args[1] string "--metrics-bind-address=:8443"
fortify-esf-operator.controllerManager.container.args[2] string "--health-probe-bind-address=:8081"
fortify-esf-operator.controllerManager.container.image.repository string "fortifydocker/esf-sjc-operator"
fortify-esf-operator.controllerManager.container.imagePullPolicy string "Always"
fortify-esf-operator.controllerManager.container.livenessProbe.httpGet.path string "/healthz"
fortify-esf-operator.controllerManager.container.livenessProbe.httpGet.port int 8081
fortify-esf-operator.controllerManager.container.livenessProbe.initialDelaySeconds int 15
fortify-esf-operator.controllerManager.container.livenessProbe.periodSeconds int 20
fortify-esf-operator.controllerManager.container.readinessProbe.httpGet.path string "/readyz"
fortify-esf-operator.controllerManager.container.readinessProbe.httpGet.port int 8081
fortify-esf-operator.controllerManager.container.readinessProbe.initialDelaySeconds int 5
fortify-esf-operator.controllerManager.container.readinessProbe.periodSeconds int 10
fortify-esf-operator.controllerManager.container.resources.limits.cpu string "500m"
fortify-esf-operator.controllerManager.container.resources.limits.memory string "128Mi"
fortify-esf-operator.controllerManager.container.resources.requests.cpu string "10m"
fortify-esf-operator.controllerManager.container.resources.requests.memory string "64Mi"
fortify-esf-operator.controllerManager.container.securityContext.allowPrivilegeEscalation bool false
fortify-esf-operator.controllerManager.container.securityContext.capabilities.drop[0] string "ALL"
fortify-esf-operator.controllerManager.nodeSelector object {} Node selector for the operator pods. Applied verbatim.
fortify-esf-operator.controllerManager.replicas int 1
fortify-esf-operator.controllerManager.securityContext.runAsNonRoot bool true
fortify-esf-operator.controllerManager.securityContext.seccompProfile.type string "RuntimeDefault"
fortify-esf-operator.controllerManager.serviceAccountName string "sjc-operator-controller-manager"
fortify-esf-operator.controllerManager.terminationGracePeriodSeconds int 10
fortify-esf-operator.controllerManager.tolerations list [] Tolerations for the operator pods. Applied verbatim.
fortify-esf-operator.crd.enable bool true
fortify-esf-operator.crd.keep bool true
fortify-esf-operator.metrics.enable bool true
fortify-esf-operator.networkPolicy.enable bool false
fortify-esf-operator.prometheus.enable bool false
fortify-esf-operator.rbac.enable bool true
global.imageTag string "26.2.1"
global.sjcApiAuthCred string "esf-sjc-credentials"
global.sjcApiAuthMode string "k8s-secret"
sjc.additionalEnvironmentVariables list [] Additional environment variables injected into the SJC container. Each entry is a standard Kubernetes env var object (name, value, valueFrom, etc.).
sjc.affinity object {} Affinity rules for the SJC pods. Applied verbatim.
sjc.containerSecurityContext object {"allowPrivilegeEscalation":false,"capabilities":{"drop":["ALL"]}} Container-level security context for the SJC container
sjc.csAuth.authCred string "" The credentials to use for authenticating to the communal storage. Required if authMode is "password" or "k8s-secret". For "password" the value is "username:password", for "k8s-secret" the value is the secret name storing the "accessKeyId", "secretAccessKey", and "sessionToken" keys. Default is "".
sjc.csAuth.authMode string "k8s-secret" The authentication mode to use for connecting to the communal storage. Supported values are: "k8s-secret", "password".
sjc.csAuth.certificateSecret object {"enabled":false,"key":"tls.crt","name":"esf-cs-cert"} The Kubernetes Secret containing the TLS certificate for the communal storage. Required if communal storage (i.e. minio) is configured to use a self-signed certificate. Default is disabled.
sjc.csAuth.certificateSecret.enabled bool false Whether the sjc.csAuth.certificateSecret is enabled. Default is false.
sjc.csAuth.certificateSecret.key string "tls.crt" The key within the Kubernetes Secret containing the TLS certificate for the communal storage. Default is "tls.crt".
sjc.csAuth.certificateSecret.name string "esf-cs-cert" The name of the Kubernetes Secret containing the TLS certificate for the communal storage. Default is "esf-cs-cert".
sjc.csAuth.region string "us-west-2" The AWS region where the communal storage is located. Required if S3 storage is used. Default is "us-west-2".
sjc.csAuth.rootUri string "s3://s3.us-west-2.amazonaws.com/993455010077-us-west-2-fod-esf-scan-job-controller" The root URI for the communal storage. Default is "s3://my-bucket/path/to/storage".
sjc.csAuth.skipTLSVerify bool false Whether to skip TLS verification when connecting to the communal storage. Default is false. Not recommended for production use.
sjc.csAuth.useSSL bool true Whether to use SSL when connecting to the communal storage. Default is true.
sjc.databaseAuth.authSecret string "esf-db-credentials" The name of the Kubernetes Secret containing the database credentials (username and password) in app mode (defaults to "esf-db-credentials")
sjc.databaseAuth.database string "sjc" The name of the database. Default is "sjc".
sjc.databaseAuth.host string "postgres-svc" The hostname of the database server. Default is "postgres-svc".
sjc.databaseAuth.initAuthSecret string "esf-db-init-credentials" The name of the Kubernetes Secret containing the database credentials (username and password) in admin mode (defaults to "esf-db-init-credentials")
sjc.databaseAuth.sslMode string "require" The SSL mode to use for the database connection. Default is "require".
sjc.image.imagePullPolicy string "Always" The image pull policy to use for the SJC deployment container. Default is "Always".
sjc.image.name string "fortifydocker/esf-sjc" The container image to use for the SJC deployment.
sjc.imagePullSecrets list [] Image pull secrets for the SJC pods. Each entry is an object with a name key.
sjc.initContainerSecurityContext object {"allowPrivilegeEscalation":false,"capabilities":{"drop":["ALL"]}} Container-level security context for the SJC init container
sjc.initImage.imagePullPolicy string "Always" The image pull policy to use for the SJC init container. Default is "Always".
sjc.initImage.name string "fortifydocker/esf-sjc-init" The container image to use for the SJC init container.
sjc.initResources.limits.memory string "512Mi" The CPU resource limit for the SJC init container. Default is not set.
sjc.initResources.requests object {"cpu":"50m","memory":"128Mi"} The resource requests and limits for the SJC init container. Default is:
sjc.initResources.requests.cpu string "50m" The CPU resource request for the SJC init container. Default is "50m".
sjc.initResources.requests.memory string "128Mi" The memory resource request for the SJC init container. Default is "128Mi".
sjc.livenessProbe.failureThreshold int 3
sjc.livenessProbe.initialDelaySeconds int 30
sjc.livenessProbe.periodSeconds int 10
sjc.livenessProbe.successThreshold int 1
sjc.livenessProbe.timeoutSeconds int 5
sjc.nodeSelector object {} Node selector for the SJC pods. Applied verbatim.
sjc.podDisruptionBudget object { minAvailable: 1 } PodDisruptionBudget spec applied verbatim. Set to {} to disable.
sjc.podSecurityContext object {"runAsNonRoot":true,"seccompProfile":{"type":"RuntimeDefault"}} Pod-level security context for the SJC deployment
sjc.priorityClassName string "" PriorityClassName for the SJC pods.
sjc.readinessProbe.failureThreshold int 3
sjc.readinessProbe.initialDelaySeconds int 10
sjc.readinessProbe.periodSeconds int 5
sjc.readinessProbe.successThreshold int 1
sjc.readinessProbe.timeoutSeconds int 3
sjc.replicaCount int 1
sjc.resources.limits object {"memory":"1Gi"} The resource limits for the SJC deployment container. Default is:
sjc.resources.limits.memory string "1Gi" The CPU resource limit for the SJC deployment container. Default is not set.
sjc.resources.requests object {"cpu":"100m","memory":"256Mi"} The resource requests and limits for the SJC deployment container. Default is:
sjc.resources.requests.cpu string "100m" The CPU resource request for the SJC deployment container. Default is "100m".
sjc.resources.requests.memory string "256Mi" The memory resource request for the SJC deployment container. Default is "256Mi".
sjc.service.type object "ClusterIP" Service IP and port configuration for the SJC API
sjc.serviceAccount.annotations object {} Annotations to add to the ServiceAccount (e.g. for IAM role binding).
sjc.serviceAccount.automountServiceAccountToken bool true Whether to automount the service account token into pods. SJC needs the K8s API, so default is true.
sjc.serviceAccount.create bool true Whether to create the ServiceAccount. Default is true.
sjc.serviceAccount.name string "fortify.esf-service-account" The name of the ServiceAccount. Default is "fortify.esf-service-account".
sjc.tolerations list [] Tolerations for the SJC pods. Applied verbatim.
sjc.topologySpreadConstraints list [] Topology spread constraints for the SJC pods. Applied verbatim.