Skip to content

[harbor] Default registry basic-auth credentials (harbor_registry_user/password) bypass Harbor RBAC #2738

Open

Description

@myasnikovdaniil

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.yaml generates the htpasswd file straight from .Values.registry.credentials.password when no existingSecret and no htpasswdString is supplied.
  • packages/apps/harbor/templates/harbor.yaml:120-124 — the cozystack wrapper configures registry.registry.resources and registry.controller.resources but does not set registry.credentials.password nor registry.credentials.existingSecret, leaving the upstream defaults active.
  • packages/system/harbor/charts/harbor/templates/core/core-cm.yaml and core-dpl.yaml reference these credentials via REGISTRY_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:

  1. A pod in any namespace that has L4 connectivity to the harbor namespace's <release>-harbor-registry Service. Cozystack does not ship a NetworkPolicy for harbor — verified: no NetworkPolicy resources under packages/apps/harbor/templates or packages/system/harbor/templates. So by default a malicious pod in a sibling tenant on the same cluster can curl http://<release>-harbor-registry.<harbor-ns>.svc:5000/v2/ with the public default credentials.
  2. 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

  1. added
    area/storageIssues or PRs related to storage (linstor, seaweedfs, bucket, velero, harbor)
    triage/acceptedIndicates an issue is ready to be actively worked on
    priority/important-soonMust be staffed and worked on either currently, or very soon, ideally in time for the next release
    on Jul 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

area/storageIssues or PRs related to storage (linstor, seaweedfs, bucket, velero, harbor)kind/bugCategorizes issue or PR as related to a bugpriority/important-soonMust be staffed and worked on either currently, or very soon, ideally in time for the next releasesecurity/confirmedConfirmed vulnerabilitysecurity/highHigh severity security findingtriage/acceptedIndicates an issue is ready to be actively worked on

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions