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
postgresqlandvalkeycharts; - 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
restrictedPod 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.axnamed1x-axin 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
postgresqlchart in the same namespace is the simplest:helm install pg tersane/postgresql --namespace registry --create-namespace \ --set bootstrap.database=registry --set bootstrap.owner=harborIts
pg-postgresql-appSecret holds the user and password, and itspg-postgresql-caSecret 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
valkeychart with a password and TLS:helm install vk tersane/valkey --namespace registry \ --set auth.enabled=true --set tls.enabled=trueValkey’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=./passwordHarbor 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:
- Create a public project, here
trivy. -
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:2Across an air gap,
oras cpthe database to a directory with--to-oci-layout, carry it over, and copy it from the directory into Harbor with--from-oci-layout. -
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:
- The database: the
postgresqlchart’s backups to S3-compatible storage, with continuous WAL archiving. - 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.
- The encryption key in the
<release>-harbor-secret-keySecret, 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 / [] |