> For the complete documentation index, see [llms.txt](https://docs.sealsecurity.io/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.sealsecurity.io/setup-containers/seal-base-images/kubernetes.md).

# Using Seal Base Images with Kubernetes and Helm

Kubernetes nodes pull images at pod-launch time, so the authentication and reference patterns from the previous two pages need to live in cluster-side configuration: an `imagePullSecret` for the credentials, and a `FROM`-equivalent image reference on each `PodSpec`. This page covers the manifest snippets and Helm value overrides for the common patterns.

## Pre-requisites

* An `imagePullSecret` that holds the credentials from [Authenticating with Seal's image registry](/setup-containers/seal-base-images/authenticating.md).
* A namespace (or set of namespaces) where workloads referencing Seal Base Images run.
* The canonical image reference for each sealed image you intend to use, supplied by your Seal account team. See [Pulling a sealed base image](/setup-containers/seal-base-images/pulling.md) for the format.

## Creating the `imagePullSecret`

The standard Kubernetes pattern for registry credentials is a `Secret` of type `kubernetes.io/dockerconfigjson`. Create it once per namespace where the workloads run:

```bash
kubectl create secret docker-registry seal-registry \
  --docker-server=<seal-registry> \
  --docker-username=<seal-username> \
  --docker-password=<seal-password> \
  --namespace=<your-namespace>
```

The secret's name (`seal-registry` here) is referenced from each `PodSpec` that pulls a sealed image. Pick a name and stick with it across the cluster; consistency matters more than the specific name.

For multi-namespace clusters, automate the secret distribution. Two common patterns:

* A controller like [Reflector](https://github.com/emberstack/kubernetes-reflector) mirrors the secret from a single source namespace to every other namespace that needs it.
* Your IaC (Helmfile, Argo CD, Flux) provisions the secret as part of the namespace bootstrap.

Either way, the secret in each namespace must hold the same Seal credentials.

## Referencing the secret from a `PodSpec`

The `PodSpec` lists pull secrets that the kubelet uses when fetching the image. For a Deployment using a sealed base image:

```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-app
  namespace: my-namespace
spec:
  replicas: 3
  selector:
    matchLabels:
      app: my-app
  template:
    metadata:
      labels:
        app: my-app
    spec:
      imagePullSecrets:
        - name: seal-registry
      containers:
        - name: app
          image: <internal-registry>/my-app:1.2.3
          # The application image's own Dockerfile uses
          # FROM <seal-registry>/python:3.12.9-slim-bullseye-20250408T180700Z-sp2
          # so the sealed base image is pulled at build time, not at runtime.
```

When the application is built on top of a Seal base image (the more common case), the cluster does not see the sealed base image at all: the build flattened it into the application image's layers, and the cluster only pulls the application image from your internal registry. The `imagePullSecret` in this case is only needed if your application image itself happens to live in Seal's registry, which is unusual.

When the cluster pulls a sealed image **directly** as a workload (also a valid pattern, especially for stateful images like Elasticsearch, Kafka, or Grafana that you run as-is), the `imagePullSecret` is what authorizes the pull:

```yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: elasticsearch
spec:
  replicas: 3
  selector:
    matchLabels:
      app: elasticsearch
  serviceName: elasticsearch
  template:
    metadata:
      labels:
        app: elasticsearch
    spec:
      imagePullSecrets:
        - name: seal-registry
      containers:
        - name: elasticsearch
          image: <seal-registry>/elasticsearch:8.13.4-20250320T091500Z-sp1
          ports:
            - containerPort: 9200
```

Here the cluster does the pull at pod-launch time, and the `imagePullSecret` is load-bearing.

## ServiceAccount-attached pull secret

For namespaces where most workloads need access to Seal's registry, attaching the pull secret to the namespace's default ServiceAccount removes the need to list it on every `PodSpec`:

```yaml
apiVersion: v1
kind: ServiceAccount
metadata:
  name: default
  namespace: my-namespace
imagePullSecrets:
  - name: seal-registry
```

Pods in that namespace that do not specify a `serviceAccountName` inherit the default ServiceAccount and its attached pull secrets. Pods that specify a different ServiceAccount need that ServiceAccount to also reference the secret.

## Helm

For Helm charts you maintain, expose the registry reference and pull secret as values:

```yaml
# values.yaml
image:
  repository: <seal-registry>/python
  tag: 3.12.9-slim-bullseye-20250408T180700Z-sp2
  pullPolicy: IfNotPresent

imagePullSecrets:
  - name: seal-registry
```

```yaml
# templates/deployment.yaml (excerpt)
spec:
  template:
    spec:
      imagePullSecrets:
        {{- toYaml .Values.imagePullSecrets | nindent 8 }}
      containers:
        - name: {{ .Chart.Name }}
          image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
          imagePullPolicy: {{ .Values.image.pullPolicy }}
```

For third-party charts that ship with `image.repository` and `image.tag` (the convention is near-universal), repointing to a sealed image is a values override:

```yaml
# values-sealed.yaml — applied with `helm install ... -f values-sealed.yaml`
image:
  repository: <seal-registry>/elasticsearch
  tag: 8.13.4-20250320T091500Z-sp1

imagePullSecrets:
  - name: seal-registry
```

The base chart stays unmodified; the values file is the single seam where the sealed image enters.

## Verifying signatures at admission time

For clusters that enforce signature-based admission policies (Kyverno, Sigstore Policy Controller, Gatekeeper with constraint templates), add a policy that requires Seal's cosign signature on any image pulled from `<seal-registry>`:

```yaml
apiVersion: policies.kyverno.io/v1alpha1
kind: ImageVerificationPolicy
metadata:
  name: require-seal-signature
spec:
  matchConstraints:
    resourceRules:
      - apiGroups: [""]
        apiVersions: ["v1"]
        operations: ["CREATE", "UPDATE"]
        resources: ["pods"]
  images:
    - name: containers
      expression: request.object.spec.containers.map(c, c.image)
  verifyImages:
    - skipImageReferences:
        - "!*<seal-registry>/*"
      attestors:
        - publicKeys:
            data: |
              -----BEGIN PUBLIC KEY-----
              <seal-cosign-public-key>
              -----END PUBLIC KEY-----
```

The full policy is registry-product-specific; the snippet above is illustrative. The intent is the same regardless: every pod whose image comes from Seal's registry must carry a valid cosign signature against Seal's published key.

## Related

* [Authenticating with Seal's image registry](/setup-containers/seal-base-images/authenticating.md): where the credentials in the pull secret come from.
* [Pulling a sealed base image](/setup-containers/seal-base-images/pulling.md): the reference format the `image:` field uses.
* [Updating to a new base image release](/setup-containers/seal-base-images/updating.md): when a newer sealed iteration ships and you want the cluster to roll over.
* [Cryptographic signing and hash verification](/trust/signing.md): the verification primitives the admission policy is built on.
