Repository navigation
[harbor] Default registry basic-auth credentials (harbor_registry_user/password) bypass Harbor RBAC #2738
Description
Summary
The cozystack harbor wrapper does not override the upstream chart's registry.credentials.username/password defaults, so the basic-auth credentials that gate the goharbor/registry-photon distribution daemon (used by core, the exporter, and the registry-controller to push/pull blobs) are the public defaults harbor_registry_user / harbor_registry_password.
This lets any pod with L4 connectivity to the harbor namespace's registry Service bypass Harbor's RBAC entirely.
Evidence
packages/system/harbor/charts/harbor/values.yaml:780-787— defaults:credentials: username: "harbor_registry_user" password: "harbor_registry_password" existingSecret: "" htpasswdString: ""
packages/system/harbor/charts/harbor/templates/registry/registry-secret.yamlgenerates the htpasswd file straight from.Values.registry.credentials.passwordwhen noexistingSecretand nohtpasswdStringis supplied.packages/apps/harbor/templates/harbor.yaml:120-124— the cozystack wrapper configuresregistry.registry.resourcesandregistry.controller.resourcesbut does not setregistry.credentials.passwordnorregistry.credentials.existingSecret, leaving the upstream defaults active.packages/system/harbor/charts/harbor/templates/core/core-cm.yamlandcore-dpl.yamlreference these credentials viaREGISTRY_CREDENTIAL_USERNAME/REGISTRY_CREDENTIAL_PASSWORD.
Threat model
The registry-photon (registry) component is an internal distribution-spec daemon that serves blob upload/download. It is fronted by core's authz layer, but the htpasswd-protected /v2/ endpoint is what core itself uses to deposit blobs. The Service is a ClusterIP in the harbor tenant namespace.
Who attacks:
- A pod in any namespace that has L4 connectivity to the harbor namespace's
<release>-harbor-registryService. Cozystack does not ship a NetworkPolicy for harbor — verified: no NetworkPolicy resources underpackages/apps/harbor/templatesorpackages/system/harbor/templates. So by default a malicious pod in a sibling tenant on the same cluster can curlhttp://<release>-harbor-registry.<harbor-ns>.svc:5000/v2/with the public default credentials. - The same applies to a pod inside the harbor namespace, e.g., a Trivy sidecar compromise.
What they get: bypass of core's RBAC. They can push and pull arbitrary OCI blobs/manifests into the registry's storage backend (S3 bucket) without ever talking to harbor-core or having a robot account. Because the registry stores content-addressable blobs that core later references by digest, a remote attacker can plant a malicious blob with a known digest and trick a privileged user to reference it via a manifest mutation, or use the registry's _uploads directory to splice content. At minimum: full read of all stored OCI artifacts, full write into the registry storage, and DoS by deleting blobs.
This is a known concern called out in the upstream chart README: when relying on the chart's built-in basic-auth, you must change the default password. The cozystack wrapper randomizes harborAdminPassword and the redis password but forgets this credential.
Suggested fix
Mirror the existing pattern used for admin-password / redis-password in packages/apps/harbor/templates/harbor.yaml:
{{- $registryPassword := randAlphaNum 32 }}
{{- if $existingSecret }}
{{- if hasKey $existingSecret.data "registry-password" }}
{{- $registryPassword = index $existingSecret.data "registry-password" | b64dec }}
{{- end }}
{{- end }}
...
stringData:
registry-password: {{ $registryPassword | quote }}
...
values:
harbor:
registry:
credentials:
password: {{ $registryPassword | quote }}Or, preferably, use htpasswdString so the bcrypt salt does not change every reconcile (the upstream README warns: re-rendering with htpasswd changes the hash on every render and triggers a registry pod restart). Stability matters: the credentials are baked into a Secret consumed by both core and registry. If the values diverge across reconciles, registry rejects core's requests until both pods restart. Use the lookup-fallback pattern.
A NetworkPolicy denying cross-namespace ingress to the registry Service is a defence-in-depth that should also be filed separately.
Environment
- Cozystack: master @ 09edf16
- Harbor chart: 1.18.2, appVersion 2.14.2
Activity