Skip to content

Kubernetes Deployment

This page gives you a working manifest for running a HarborGuard sensor in Kubernetes. There is no official Helm chart at this time - if and when one ships, this page will be updated.

Container runtime requirements

The sensor currently requires access to a Docker-compatible daemon. Internally it shells out to the docker CLI to pull and inspect images, so it talks to a socket that speaks the Docker Engine API.

This means nodes whose container runtime is containerd or CRI-O are not directly supported by mounting the runtime socket. You cannot point the sensor at /run/containerd/containerd.sock or /var/run/crio/crio.sock and have it work - the wire protocols are different.

Be aware that containerd is the default runtime on Kubernetes 1.24+, and on most managed clusters (EKS, GKE, AKS) since 2022. If you have not deliberately provisioned a Docker-based node pool, assume your nodes are containerd.

Three workarounds are available today:

  1. Run the sensor on a dedicated VM or node pool with Docker installed. This is the recommended option for air-gapped clusters or organizations that want to keep scanning fully on-prem. Target the node pool with nodeSelector (shown in the manifest below). For most workloads a single small VM running the sensor as a systemd service is simpler than running it in Kubernetes at all - see the Docker sensor guide.
  2. Use a Docker-in-Docker sidecar. Run docker:dind alongside the sensor in the same pod and point the sensor at the dind socket. This works but has real costs: dind requires a privileged container (securityContext.privileged: true), consumes additional CPU/memory, and means images get pulled twice (once into dind, once for inspection). Treat it as a last resort.
  3. Use HarborGuard cloud scanning. If your cluster can reach https://harborguard.co over HTTPS, you can skip the in-cluster sensor entirely and let our managed scanners pull from your registries. This is the lowest-friction option for clusters where the only reason to self-host was the lack of a containerd backend.

Native containerd support is on the roadmap. Until it ships, pick one of the three options above.

Prerequisites

  • A namespace to put the sensor in (harborguard-system recommended).
  • A node pool whose container runtime exposes /var/run/docker.sock. Pure containerd nodes without dockershim do not work for socket-based pulling - see Containerd note.
  • An API key with the developer role.
  • Outbound HTTPS from the cluster to your HarborGuard URL.

Complete manifest

Save as harborguard-sensor.yaml and kubectl apply -f harborguard-sensor.yaml.

The manifest below mounts /var/run/docker.sock from the host via hostPath. Confirm the target node actually runs the Docker daemon - on containerd or CRI-O nodes this path does not exist (or points at something incompatible) and the sensor will fail at startup. Run kubectl get nodes -o wide and check the CONTAINER-RUNTIME column before applying.

apiVersion: v1
kind: Namespace
metadata:
  name: harborguard-system
---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: harborguard-sensor
  namespace: harborguard-system
---
apiVersion: v1
kind: Secret
metadata:
  name: harborguard-sensor
  namespace: harborguard-system
type: Opaque
stringData:
  HARBORGUARD_URL: "https://harborguard.co"
  HARBORGUARD_API_KEY: "hg_live_xxxxxxxxxxxxxxxx"
  # Optional: pre-baked registry creds. Or mount a docker config secret.
  REGISTRY_URL: "ghcr.io"
  REGISTRY_USERNAME: "ci-bot"
  REGISTRY_PASSWORD: "ghp_xxxxxxxxxxxxxxxx"
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: harborguard-sensor
  namespace: harborguard-system
  labels:
    app.kubernetes.io/name: harborguard-sensor
spec:
  replicas: 1
  selector:
    matchLabels:
      app.kubernetes.io/name: harborguard-sensor
  strategy:
    type: Recreate         # only one sensor talks to one socket; avoid two pods on the same node
  template:
    metadata:
      labels:
        app.kubernetes.io/name: harborguard-sensor
    spec:
      serviceAccountName: harborguard-sensor
      # Pin to nodes that expose /var/run/docker.sock
      nodeSelector:
        harborguard.co/runtime: docker
      securityContext:
        runAsNonRoot: false   # needs access to the docker socket, owned by root:docker on most distros
        seccompProfile:
          type: RuntimeDefault
      containers:
        - name: sensor
          image: ghcr.io/harborguard/sensor:latest
          imagePullPolicy: IfNotPresent
          envFrom:
            - secretRef:
                name: harborguard-sensor
          env:
            - name: SENSOR_NAME
              valueFrom:
                fieldRef:
                  fieldPath: spec.nodeName
          resources:
            requests:
              cpu: "500m"
              memory: "1Gi"
            limits:
              cpu: "2"
              memory: "4Gi"
          securityContext:
            # The sensor needs the docker group to talk to the socket.
            # Adjust the GID to match your node (`getent group docker`).
            runAsUser: 0
            allowPrivilegeEscalation: false
            capabilities:
              drop: ["ALL"]
            readOnlyRootFilesystem: false
          volumeMounts:
            - name: docker-sock
              mountPath: /var/run/docker.sock
            - name: scanner-cache
              mountPath: /var/cache/harborguard
          livenessProbe:
            exec:
              command: ["pgrep", "-f", "sensor"]
            initialDelaySeconds: 30
            periodSeconds: 30
            timeoutSeconds: 5
            failureThreshold: 3
          readinessProbe:
            exec:
              command: ["pgrep", "-f", "sensor"]
            initialDelaySeconds: 10
            periodSeconds: 10
      volumes:
        - name: docker-sock
          hostPath:
            path: /var/run/docker.sock
            type: Socket
        - name: scanner-cache
          emptyDir:
            sizeLimit: 5Gi

Label the eligible nodes once:

kubectl label node <node-name> harborguard.co/runtime=docker

Why these securityContext choices

SettingWhy
runAsUser: 0The Docker socket is owned by root:docker on most distros. Easiest portable answer is to run the container as root and drop all capabilities. If your node has a known docker GID, set runAsUser: 1000 and runAsGroup: <docker-gid> instead.
allowPrivilegeEscalation: falseSensor never needs to gain privileges at runtime.
capabilities.drop: ["ALL"]The sensor needs no Linux capabilities beyond what root already grants for socket I/O.
seccompProfile: RuntimeDefaultStandard syscall sandbox.
hostPath socket mountRequired so the sensor can pull and inspect images via the node's runtime.

Containerd note

If your node uses containerd without a Docker socket, the hostPath: /var/run/docker.sock mount will fail. See Container runtime requirements at the top of this page for the supported workarounds.

DaemonSet vs Deployment

Use a Deployment with a single replica when the sensor scans images from a registry, regardless of where the images run. This is the common case.

Use a DaemonSet only if you want one sensor per node for node-local image scanning. Note that the atomic job-claim model already distributes work safely across replicas, so per-node DaemonSet is rarely needed.

Verifying

kubectl -n harborguard-system get pods
kubectl -n harborguard-system logs -l app.kubernetes.io/name=harborguard-sensor -f

You should see register / heartbeat / claim lines. The sensor will appear in Sensors in the UI as online.

Troubleshooting

SymptomLikely cause
Pod logs show docker: command not found or cannot connect to /var/run/docker.sockThe node is not running the Docker daemon. Check kubectl get nodes -o wide - if CONTAINER-RUNTIME is containerd://... or cri-o://..., follow Container runtime requirements.
Pod stuck in ContainerCreating with a hostPath error/var/run/docker.sock does not exist on the node. Same root cause as above.
Sensor logs permission denied on the socketThe container UID lacks access to the docker group. Set runAsUser: 0 or align runAsGroup with the node's docker GID (getent group docker).
Sensor never appears in Sensors UICheck HARBORGUARD_URL and that the API key has the developer role. Outbound HTTPS to harborguard.co (or your self-hosted URL) must be permitted.

Helm

There is no official chart. If a community chart appears it will be linked from this page. For now, treat the manifest above as a template and parameterize with kustomize or your own values.

On this page