Bahriya Tersane

Documentation / harbor

Harbor

Installs Harbor, a container registry: projects with members and robot accounts, vulnerability scanning with Trivy, signatures kept with the images they sign, replication to and from other registries, and proxy caching of them.

The chart provides:

  • Harbor 2.16, for amd64 and arm64, with its database in a PostgreSQL of your own and its cache in a Valkey of your own, such as releases of the postgresql and valkey charts;
  • images in S3-compatible storage, which every registry replica shares, or on a volume for a single small installation;
  • TLS on every connection between Harbor’s components, with a certificate authority of the release’s own made with cert-manager, and TLS to the database and Valkey with their certificates checked;
  • the administrator’s password, and the database, Valkey and storage credentials, taken from Secrets you create, which the chart never creates, changes or prints;
  • vulnerability scanning that works without internet access, from a database copied into Harbor itself;
  • an Ingress, a NetworkPolicy for each component, and optional Prometheus metrics;
  • Harbor’s images from the Mamluk registry, pinned by digest, and every Pod meeting the restricted Pod Security Standard with a read-only root filesystem.

Prerequisites

  • Kubernetes 1.33 or later.
  • cert-manager, for the certificates between the components. It is included with the Bahriya Kubernetes Engine; on other Kubernetes, install it first.
  • A pull secret for 1x.ax named 1x-ax in the release’s namespace, created from an image token as described in the access documentation.
  • A PostgreSQL database, 14 or later, owned by the user Harbor connects as. A release of the postgresql chart in the same namespace is the simplest:

    helm install pg tersane/postgresql --namespace registry --create-namespace \
      --set bootstrap.database=registry --set bootstrap.owner=harbor
    

    Its pg-postgresql-app Secret holds the user and password, and its pg-postgresql-ca Secret the certificate authority Harbor checks the server against.

  • A Valkey, standalone or with Sentinel, whose logical databases 0, 1, 2 and 5 no other application uses. A release of the valkey chart with a password and TLS:

    helm install vk tersane/valkey --namespace registry \
      --set auth.enabled=true --set tls.enabled=true
    

    Valkey’s cluster mode is not supported by Harbor.

  • A bucket in S3-compatible storage and a key that can read and write it, in a Secret:

    kubectl --namespace registry create secret generic harbor-s3 \
      --from-literal=accesskey=<access key> --from-file=secretkey=./secret-key
    
  • A Secret holding the administrator’s password:

    kubectl --namespace registry create secret generic harbor-admin \
      --from-file=password=./password
    

    Harbor reads it once, when it first creates its database. Change the password afterwards in Harbor itself.

Installation

helm install harbor tersane/harbor --namespace registry \
  --set externalURL=https://registry.example.com \
  --set admin.existingSecret=harbor-admin \
  --set database.host=pg-postgresql-rw \
  --set database.existingSecret=pg-postgresql-app \
  --set database.caSecret=pg-postgresql-ca \
  --set cache.addr=vk-valkey:6379 \
  --set cache.existingSecret=vk-valkey \
  --set cache.tls.enabled=true --set cache.tls.caSecret=vk-valkey-tls \
  --set storage.s3.bucket=<bucket> \
  --set storage.s3.regionEndpoint=https://<storage endpoint> \
  --set storage.s3.existingSecret=harbor-s3 \
  --set ingress.className=<class> --set ingress.tlsSecret=<certificate Secret> \
  --version <chart version>

The release’s resources are named <release>-harbor (here harbor-harbor), or after the release alone if its name already contains harbor. Core creates and migrates its tables when it first starts, which takes a few minutes. Check that every Pod is ready:

kubectl --namespace registry get pods -l app.kubernetes.io/instance=harbor

Then sign in at the external URL as admin.

externalURL is the address every client uses: the one docker login is given, the one Harbor names in the tokens it issues, and the one the portal shows in pull commands. Changing it later invalidates the tokens already issued.

Components

Component Replicas Role
core 2 The API, the token service, authentication; proxies the registry API
portal 2 The web interface
registry 2 Stores and serves image content; with its controller in the same Pod
job service 2 Replication, scanning, garbage collection and retention jobs
Trivy 1 Vulnerability scanning, with its database on a volume

Every replica of core, the portal, the registry and the job service is interchangeable: their state is in PostgreSQL, Valkey and the storage. Job logs are kept in the database, so any replica shows any job’s log.

Database

Harbor keeps projects, users, robots, artifacts’ metadata, scan results and job history in PostgreSQL. database.sslmode is verify-full by default: the server’s certificate must be signed by the authority in database.caSecret and name database.host. require encrypts without checking the certificate.

While PostgreSQL is unavailable, Harbor is unavailable: pushes are refused, and its health API does not answer. Harbor recovers by itself once PostgreSQL is back.

database.maxOpenConns (default 100) is the limit of each core Pod, so the server must accept at least core.replicas times as many.

Cache

Harbor uses Valkey for its job queue, the registry’s layer cache, sessions and Trivy’s queue. With cache.tls.enabled, the certificate must be signed by the authority in cache.tls.caSecret and name the host in cache.addr. With Sentinel, set cache.addr to the Sentinels, comma-separated, and cache.sentinelMasterSet to the primary’s name.

Harbor takes Valkey’s password inside a URL, so it must use only letters, digits and -._~. The valkey chart’s generated passwords do.

While Valkey is unavailable, Harbor is unavailable. Core cannot answer requests without it, fails its probes and is restarted until Valkey is back; pushes and pulls are refused. Harbor recovers by itself within a minute of Valkey’s return. Jobs queued but not yet run are lost if Valkey loses its data. Run Valkey with Sentinel where that matters.

Image storage

With storage.type: s3 (the default), image content is kept in the bucket and every registry replica shares it. storage.s3.regionEndpoint names storage that is not AWS; buckets there are addressed by path. An endpoint with a private certificate needs its authority in storage.s3.caSecret.

By default every download passes through Harbor, as an installation whose clients cannot reach the storage needs. With storage.redirect: true, clients download layers from the storage directly, through a signed URL, which reduces the load on the registry but requires clients to reach the storage.

With storage.type: filesystem, content is kept on one volume of storage.filesystem.size. On a ReadWriteOnce volume the registry has one replica and is stopped before it is replaced on an upgrade; a ReadWriteMany volume allows more. The volume is kept when the release is uninstalled.

Exposing Harbor

The chart’s Ingress serves the host in externalURL: /v2/, /service/, /api/ and /c/ go to core, everything else to the portal. Layers are uploaded in requests of any size, so the Ingress controller must not limit the request body or buffer it on disk with a small limit.

With internal TLS, core and the portal speak HTTPS only, so the Ingress controller must connect to them over HTTPS and trust, or not check, the release’s authority. For Kong:

ingress:
  className: kong
  serviceAnnotations:
    konghq.com/protocol: https

Without an Ingress controller, core.service.type: NodePort exposes the registry API, the token service and the API on every node: enough for docker login, push and pull, not for the portal. Core then serves its own certificate, from the release’s authority, which must name the address clients use: add it to core.extraDNSNames, and give clients the authority from the <release>-harbor-ca Secret, key ca.crt.

Nodes pulling from Harbor

Kubernetes nodes pull from Harbor with an image pull secret holding a robot account’s name and secret:

kubectl --namespace apps create secret docker-registry harbor-pull \
  --docker-server=registry.example.com \
  --docker-username='robot$app+puller' --docker-password-stdin

A node whose containerd does not trust Harbor’s certificate, such as one in an air-gapped network with a private authority, needs the authority in /etc/containerd/certs.d/<host>/ca.crt and, beside it, hosts.toml:

server = "https://registry.example.com"

[host."https://registry.example.com"]
  ca = "/etc/containerd/certs.d/registry.example.com/ca.crt"

Internal TLS and trusted authorities

With internalTLS.enabled (the default), every component listens on HTTPS with a certificate from the release’s own authority, made with cert-manager, and every component checks the others against it. The certificates are valid for a year and renewed a month before they expire; a component reads its certificate when it starts, so restart the release within that month:

kubectl --namespace registry rollout restart deployment,statefulset -l app.kubernetes.io/instance=harbor

Harbor’s components trust the system’s authorities and these: the release’s own, database.caSecret, cache.tls.caSecret, storage.s3.caSecret, and each Secret in trustedCA. Add to trustedCA the authorities of replication endpoints, proxied registries and an OIDC provider with private certificates.

Authentication

Harbor’s users are kept in its database by default. To sign in through an OIDC provider, or LDAP, set Harbor’s settings in the portal under Administration > Configuration, or declare them in a Secret:

kubectl --namespace registry create secret generic harbor-settings --from-file=json=./settings.json
{
  "auth_mode": "oidc_auth",
  "oidc_name": "Corporate SSO",
  "oidc_endpoint": "https://sso.example.com/realms/corp",
  "oidc_client_id": "harbor",
  "oidc_client_secret": "<secret>",
  "oidc_scope": "openid,profile,email,offline_access",
  "oidc_user_claim": "preferred_username",
  "oidc_auto_onboard": true,
  "oidc_verify_cert": true
}
settings:
  existingSecret: harbor-settings

Harbor applies the Secret each time core starts. While it is set, no setting can be changed in the portal or the API, only in the Secret. The authentication mode can be changed from the database to OIDC or LDAP only while Harbor has no user but admin.

The OIDC provider redirects to <externalURL>/c/oidc/callback, which must be registered with it. A user signed in through OIDC uses the CLI secret shown in their profile for docker login.

Robot accounts are the credentials of pipelines and nodes. A robot of a project has only the permissions it is given, in that project, and none in any other.

Scanning without internet access

Trivy downloads its vulnerability database, an OCI artifact, from trivy.dbRepository before a scan when the copy on its volume is due for an update. Without internet access, copy the database into a project of this Harbor and name that project instead:

  1. Create a public project, here trivy.
  2. On a machine with internet access, copy the database into Harbor, with ORAS, as a user who can push to the project:

    oras cp mirror.gcr.io/aquasec/trivy-db:2 registry.example.com/trivy/trivy-db:2
    

    Across an air gap, oras cp the database to a directory with --to-oci-layout, carry it over, and copy it from the directory into Harbor with --from-oci-layout.

  3. Point Trivy at the project:

    trivy:
      dbRepository:
        - registry.example.com/trivy/trivy-db
      javaDBRepository:
        - registry.example.com/trivy/trivy-java-db
    

To load a new database, repeat step 2; Trivy downloads it before its next scan. Without a database in the project, scans end in an error. Aqua Security publishes a new database every few hours; vulnerabilities published after the copy you load are not found until you load the next.

The Java database, used for JAR files, is copied in the same way from mirror.gcr.io/aquasec/trivy-java-db:1, or skipped with trivy.skipJavaDBUpdate: true. trivy.offlineScan: true also stops Trivy asking the internet about Java dependencies while scanning.

Signatures

Harbor keeps cosign and Notation signatures, SBOMs and other OCI accessories with the image they refer to: they are listed with it, deleted with it, replicated with it and kept by garbage collection while it is.

Replication and proxy cache

Replication copies images, with their accessories, to or from other registries: Harbor, Docker Hub, the cloud registries and any registry with the OCI distribution API. A proxy cache project serves images from another registry and keeps them, so nodes pull from Harbor alone. Both are set up in the portal or the API; the job service runs them.

Garbage collection

Deleting a tag or an artifact removes it from Harbor’s database, not from the storage. Garbage collection, scheduled under Administration > Clean Up or run on demand, deletes the content no artifact refers to. It never deletes content an artifact or accessory still refers to, and leaves alone content uploaded in the last two hours (core.gcTimeWindowHours), so that pushes in progress are not disturbed. Pushes may continue while it runs.

Upgrades

Each chart version pins Harbor’s images by digest. Core migrates the database when it starts: the first core of the new version to start takes a lock on the database, migrates it and releases it, and the others wait. A migration is not reversed by rolling back the chart, so back up the database first. Read Harbor’s release notes before moving to a new minor version.

Upgrading PostgreSQL itself is the postgresql chart’s: Harbor needs no step.

Backups and restore

A complete backup is three things, taken together:

  1. The database: the postgresql chart’s backups to S3-compatible storage, with continuous WAL archiving.
  2. The image storage: versioning or replication of the bucket, by the storage itself, or a copy of the bucket taken after the database backup. A storage copy older than the database backup lacks content the database refers to.
  3. The encryption key in the <release>-harbor-secret-key Secret, with which Harbor encrypts the credentials it stores: replication endpoints’ passwords and the OIDC client secret. Keep a copy outside the cluster.

To restore, recover the database into a new postgresql release, make the storage copy available, create the key’s Secret, and install Harbor on them with secretKey.existingSecret naming it. Harbor’s own internal secrets are generated anew and need no backup.

Network policy

Each component admits only its callers: core admits the job service, Trivy and clients; the portal admits clients; the registry admits core and the job service; the job service admits core; Trivy admits core and the job service, which submits the scans. Clients are any source unless networkPolicy.ingressFrom names the Ingress controller, which a NodePort does not allow. Egress is not limited: the database, Valkey, the storage, OIDC providers and replication endpoints are of your choosing.

Metrics

metrics.enabled adds Prometheus metrics on port 8001 of core, the registry and the job service, at /metrics, admitted from metrics.from.

Pod Security

Every Pod meets the restricted Pod Security Standard: Harbor runs as user 10000 with a read-only root filesystem, no privilege escalation, every capability dropped, the runtime’s default seccomp profile and no service account token.

Harbor’s images add trusted authorities by rewriting the system’s bundle when they start, which a read-only root filesystem does not allow. The chart instead starts each component directly: an init container writes the system’s bundle and the trusted authorities into a volume in memory, and the components read it.

Uninstalling

helm uninstall harbor --namespace registry

The encryption key’s Secret, and the volume with storage.type: filesystem, are kept. The database, Valkey and the bucket are yours, and are untouched.

Parameters

Harbor

Parameter Description Default
externalURL The address clients reach Harbor at, with its scheme; required ""
images.<component>.repository / .tag Each component’s image 1x.ax/tersane/core/harbor/... / the release’s
imagePullPolicy Pull policy of every image IfNotPresent
imagePullSecrets Pull secrets of every Pod [{name: 1x-ax}]
admin.existingSecret / admin.passwordKey The Secret with the administrator’s password; required "" / password
secretKey.existingSecret / secretKey.key A Secret with the 16-character encryption key; empty generates one "" / secretKey
settings.existingSecret / settings.key A Secret with Harbor’s settings as JSON "" / json
logLevel debug, info, warning or error info
clusterDomain The cluster’s DNS domain cluster.local

Database and cache

Parameter Description Default
database.host / database.port PostgreSQL; the host is required "" / 5432
database.name The database Harbor owns registry
database.existingSecret The Secret with the user and password; required ""
database.usernameKey / database.passwordKey Its keys username / password
database.sslmode verify-full, verify-ca, require or disable verify-full
database.caSecret / database.caKey The server’s authority; required to verify "" / ca.crt
database.maxIdleConns / database.maxOpenConns Connections of each core Pod 50 / 100
cache.addr Valkey’s host:port, or the Sentinels’; required ""
cache.sentinelMasterSet The primary’s name, with Sentinel ""
cache.existingSecret / cache.passwordKey The Secret with Valkey’s password "" / password
cache.username Valkey’s ACL user ""
cache.tls.enabled TLS to Valkey false
cache.tls.caSecret / cache.tls.caKey Valkey’s authority "" / ca.crt
cache.databases.core / .jobservice / .registry / .trivy The logical databases Harbor uses 0 / 1 / 2 / 5

Storage and trust

Parameter Description Default
storage.type s3 or filesystem s3
storage.s3.bucket / storage.s3.region The bucket and its region "" / us-east-1
storage.s3.regionEndpoint The endpoint of storage that is not AWS ""
storage.s3.rootDirectory A prefix within the bucket ""
storage.s3.existingSecret The Secret with the access and secret keys; required ""
storage.s3.accessKeyKey / storage.s3.secretKeyKey Its keys accesskey / secretkey
storage.s3.secure HTTPS to the storage true
storage.s3.caSecret / storage.s3.caKey The storage’s authority "" / ca.crt
storage.redirect Clients download layers from the storage directly false
storage.filesystem.size / .storageClass / .accessMode The volume 100Gi / "" / ReadWriteOnce
storage.filesystem.existingClaim A claim of your own instead ""
trustedCA Further authorities: [{secret, key}] []
internalTLS.enabled TLS between the components true

Components

Parameter Description Default
core.replicas / core.resources Core 2 / see values.yaml
core.service.type / core.service.nodePort ClusterIP or NodePort ClusterIP / ""
core.extraDNSNames Further names on core’s certificate []
core.gcTimeWindowHours Hours garbage collection leaves new content alone 2
portal.replicas / portal.resources The portal 2 / see values.yaml
registry.replicas / registry.resources / registry.controllerResources The registry and its controller 2 / see values.yaml
registry.uploadPurging.enabled / .age / .interval Removal of unfinished uploads true / 168h / 24h
jobservice.replicas / jobservice.maxJobWorkers / jobservice.resources The job service 2 / 10 / see values.yaml
trivy.enabled / trivy.replicas / trivy.resources Trivy true / 1 / see values.yaml
trivy.dbRepository / trivy.javaDBRepository Where Trivy downloads its databases mirror.gcr.io, then ghcr.io
trivy.skipUpdate / trivy.skipJavaDBUpdate Never download a database false / false
trivy.offlineScan No internet requests while scanning false
trivy.insecure Accept a database repository without a trusted certificate false
trivy.severity / trivy.vulnType / trivy.ignoreUnfixed / trivy.timeout Scan settings see values.yaml
trivy.redisPool.maxActive / trivy.redisPool.maxIdle Trivy’s connections to Valkey; enough for one job per platform of an image 20 / 10
trivy.storage.size / trivy.storage.storageClass Trivy’s volume 5Gi / ""
proxy.httpProxy / proxy.httpsProxy / proxy.noProxy An HTTP proxy for core, the job service and Trivy ""
nodeSelector / tolerations Scheduling of every Pod {} / []
podSecurityContext / securityContext Pod and container security see values.yaml

Access and metrics

Parameter Description Default
ingress.enabled / ingress.className The Ingress true / ""
ingress.annotations Its annotations {}
ingress.tlsSecret The host’s certificate ""
ingress.serviceAnnotations Annotations on the core and portal Services {}
networkPolicy.enabled A NetworkPolicy for each component true
networkPolicy.ingressFrom Sources admitted to core and the portal []
metrics.enabled / metrics.from Prometheus metrics, and who may read them false / []