Files
Aleksei Sviridkin 889bb79236 feat(harbor): restrict the portal and API to listed source ranges (#4300)
## What this PR does

Adds `managementWhitelist` to the Harbor app, a list of IP addresses or
CIDR ranges allowed to reach the web portal and the Harbor API, and I
made it a typed list on purpose. The obvious alternative, a free-form
`ingress.annotations` map, gives tenants a way into the shared ingress
controller. The platform ingress-nginx runs with
`allowSnippetAnnotations: true` (the kubernetes app needs a
`configuration-snippet`), and v1.11.5 defaults `annotations-risk-level`
to `Critical`, so it accepts any annotation, `server-snippet` included.
Tenants can't create Ingress objects themselves, but they can write any
field of an app spec. An annotation map on the Harbor spec would let any
tenant put arbitrary nginx config into a controller that serves every
tenant. A typed list is also what the repo already does for this:
`filer.whitelist` in `extra/seaweedfs`, `whitelist` in `extra/ingress`
and `tcp-balancer`.

The use case is a registry that serves customers: pulls and pushes come
from anywhere, the admin UI and API should answer only to an office or
VPN range. Right now the portal, `/api/v2.0` and `/v2/` sit on one
Ingress and one public host, and a tenant can't restrict any of them.

With a non-empty list the existing Ingress gets
`nginx.ingress.kubernetes.io/whitelist-source-range`, and `/v2` and
`/service/token` move to a second Ingress, `<release>-registry-ingress`,
on the same host. ingress-nginx applies that annotation only to the
locations of the Ingress that carries it, so those two stay open. They
are what a registry client needs: Harbor answers an anonymous `/v2/`
request with a Bearer challenge whose realm is `/service/token` on the
same host. Everything else under `/service/` is
`/service/notifications/*`, internal job callbacks that the bundled
nginx already answers with 404, so it stays on the restricted side. The
second Ingress uses the same TLS secret and has no cert-manager
annotations, otherwise ingress-shim gets two owners for one Certificate.
The HTTP-01 solver is its own Ingress and is not restricted. With the
default empty list the chart renders exactly what it renders today.

An HTTPRoute can't express a source restriction, so in Gateway API mode
a non-empty list fails the render. Dropping it silently would leave the
portal public while the spec says it is restricted.

Each entry has to be an IP address or CIDR range, and a bad one fails
the render. ingress-nginx reads an empty `whitelist-source-range` as no
restriction and turns an unparseable one into a 503 for the whole
location, and the values schema generator can't validate list items.

Three things users should know. Harbor-to-Harbor replication that pulls
from this registry goes through its API, so the other registry's address
has to be on the list. The check uses the client address that
ingress-nginx sees: with `externalTrafficPolicy: Local` or PROXY
protocol that is the real client, behind another proxy it is the proxy.
And for the portal and API the list replaces the tenant ingress
`whitelist` rather than narrowing it.

Disabling the portal is not here. The vendored Harbor chart (1.19.1)
always renders the portal Deployment and its internal nginx always
proxies `/` to it, so there is no small switch to wire.

The new Ingress is added to the dashboard Role and to the ingress
selector in the ApplicationDefinition.

### Screenshots

No UI code change. The field reaches the dashboard form through the
regenerated schema and `keysOrder`.

### Downstream repositories

The terraform provider models Harbor by hand, so the new field needs a
follow-up there: cozystack/terraform-provider-cozystack#46. The website
reference page is generated from `README.md` on release. Nothing else in
the trigger map is touched: no package added or renamed, no platform,
installer or `hack/` change.

- [ ] No downstream repository is affected by this change
- [ ] [cozystack/website](https://github.com/cozystack/website) -
follow-up:
- [x]
[cozystack/terraform-provider-cozystack](https://github.com/cozystack/terraform-provider-cozystack)
- follow-up:
https://github.com/cozystack/terraform-provider-cozystack/issues/46
- [ ]
[cozystack/ansible-cozystack](https://github.com/cozystack/ansible-cozystack)
- follow-up:
- [ ] [cozystack/ccp](https://github.com/cozystack/ccp) - follow-up:
- [ ] [cozystack/talm](https://github.com/cozystack/talm) - follow-up:
- [ ] [cozystack/cozyhr](https://github.com/cozystack/cozyhr) -
follow-up:
- [ ] [cozystack/cozy-proxy](https://github.com/cozystack/cozy-proxy) -
follow-up:
- [ ]
[cozystack/cozystack-telemetry-server](https://github.com/cozystack/cozystack-telemetry-server)
- follow-up:
- [ ]
[cozystack/external-apps-example](https://github.com/cozystack/external-apps-example)
- follow-up:
- [ ] [cozystack/examples](https://github.com/cozystack/examples) -
follow-up:
- [ ] [cozystack/community](https://github.com/cozystack/community) -
follow-up:

### Release note

```release-note
feat(harbor): add `managementWhitelist`, a list of IP addresses or CIDR ranges allowed to reach the Harbor web portal and API. Only `/v2/` and `/service/token` stay reachable from any address. The default is an empty list, which keeps the current behaviour. Not supported with Gateway API publishing: a non-empty list fails the render there. Each entry must be an IP address or CIDR range; for the portal and API the list replaces the tenant ingress `whitelist`.
```
2026-10-01 17:39:56 +03:00
..

Managed Harbor Container Registry

Harbor is an open-source trusted cloud-native registry project that stores, signs, and scans content.

Prerequisites

The Cozystack Harbor app stores its registry data exclusively in S3-compatible object storage: the chart pins the registry backend to S3 and exposes no filesystem option. That bucket is provisioned through COSI (objectstorage.k8s.io) from a SeaweedFS deployment, so before deploying Harbor the tenant must have SeaweedFS available — enabled on the same tenant or inherited from a parent tenant (the resolved class is propagated down the tenant tree, surfaced as the namespace.cozystack.io/seaweedfs namespace label).

Enable it by setting seaweedfs: true on the tenant (or a parent tenant):

seaweedfs: true

Without object storage in the tenant chain, Harbor cannot provision its registry bucket: the <release>-registry BucketClaim/BucketAccess never produces the <release>-registry-bucket credentials secret, so the Harbor HelmRelease stays unreconciled, waiting on BucketInfo.

storageClass is annotated as immutable in the chart schema — see docs/storage-immutability.md for the contract and which consumers enforce it.

Parameters

Common parameters

Name Description Type Value
host Hostname for external access to Harbor (defaults to 'harbor' subdomain for the tenant host). string ""
managementWhitelist IP addresses or CIDR ranges allowed to reach the Harbor web portal and API. /v2/ and /service/token, which docker login, image pulls and pushes use, are not restricted by this list. For the portal and API it replaces the tenant ingress whitelist rather than narrowing it, and it is matched against the client address the ingress controller sees, which is a proxy or a forwarded header rather than the real client when something in front of the controller rewrites it. Empty allows every address. Requires Ingress publishing; rejected when the tenant publishes through Gateway API. []string []
storageClass StorageClass used to store the data. string ""

Component configuration

Name Description Type Value
core Core API server configuration. object {}
core.resources Explicit CPU and memory configuration. When omitted, the preset defined in resourcesPreset is applied. object {}
core.resources.cpu Number of CPU cores allocated. quantity ""
core.resources.memory Amount of memory allocated. quantity ""
core.resourcesPreset Default sizing preset used when resources is omitted. string t1.small
registry Container image registry configuration. object {}
registry.resources Explicit CPU and memory configuration. When omitted, the preset defined in resourcesPreset is applied. object {}
registry.resources.cpu Number of CPU cores allocated. quantity ""
registry.resources.memory Amount of memory allocated. quantity ""
registry.resourcesPreset Default sizing preset used when resources is omitted. string t1.small
jobservice Background job service configuration. object {}
jobservice.resources Explicit CPU and memory configuration. When omitted, the preset defined in resourcesPreset is applied. object {}
jobservice.resources.cpu Number of CPU cores allocated. quantity ""
jobservice.resources.memory Amount of memory allocated. quantity ""
jobservice.resourcesPreset Default sizing preset used when resources is omitted. string t1.nano
trivy Trivy vulnerability scanner configuration. object {}
trivy.enabled Enable or disable the vulnerability scanner. bool true
trivy.size Persistent Volume size for vulnerability database cache. quantity 5Gi
trivy.resources Explicit CPU and memory configuration. When omitted, the preset defined in resourcesPreset is applied. object {}
trivy.resources.cpu Number of CPU cores allocated. quantity ""
trivy.resources.memory Amount of memory allocated. quantity ""
trivy.resourcesPreset Default sizing preset used when resources is omitted. string t1.nano
database PostgreSQL database configuration. object {}
database.size Persistent Volume size for database storage. quantity 5Gi
database.replicas Number of database instances. int 2
redis Redis cache configuration. object {}
redis.size Persistent Volume size for cache storage. quantity 1Gi
redis.replicas Number of Redis replicas. int 2