# Risk context for Harbor (goharbor/harbor), v2.15.2.
#
# This file lives at `.k9security/risk-context.yaml` in the repository. It is
# the input YOU write: it records how critical this system is, where its code
# actually runs, who is trusted, and which risks this deployment carries
# deliberately. k9 reads it when scoring dependency alerts and reports which
# revision of it produced the verdicts.
#
schema_version: "1.0"

project:
  name: harbor
  summary: >-
    Harbor container registry (CNCF), v2.15.2. Stores and serves container
    images, Helm charts and OCI artifacts; provides project-scoped RBAC,
    replication, webhooks and vulnerability scanning. Deployed as a set of
    containers built from this repository.
  open_source: true

reviewed:
  date: 2026-09-16
  by: skuenzli@k9security.io
  commit: 080b0220574cc853ae1e2946ce7a5610ba855757

# ---------------------------------------------------------------------------
# HOW THE FIVE GO CONTEXTS ARE DISTINGUISHED
#
# core, jobservice, registryctl, exporter and db-migrator are five separate
# main packages built from ONE Go module. A dependency alert names
# /src/go.mod, so the path CANNOT say which services a vulnerable module
# reaches, so every Go context below lists the same manifest deliberately.
#
# The question is decidable, not a judgement call: `go list -deps <main pkg>`
# resolves offline against the vendored tree in /src/vendor and reports the
# exact module closure linked into each binary. Measured at the reviewed
# commit: 230 distinct third-party modules across the five binaries, of which
# 90 (39%) reach exactly one service and 64 (28%) reach all five.
#
# A client with no shell cannot run that command. It should bind
# conservatively to all five rather than guess which ones apply.
# ---------------------------------------------------------------------------

execution_contexts:
  - id: core
    default: true
    applies_to: >-
      Go dependencies linked into the core API binary (/src/core). 210
      third-party modules at the reviewed commit, 70 of them reached by no
      other service. Shares /src/go.mod with the other four Go contexts -- see
      the note above. As the default context, it also takes any file matched by
      no other context, including the shared library tree under /src/lib,
      /src/common, /src/pkg, /src/controller and /src/server.
    frameworks:
      - name: Beego
        roles: [web, api]
      - name: go-openapi/runtime
        roles: [api]
      - name: gorilla/mux
        roles: [web, api]
      - name: casbin
        roles: [api]
    paths:
      include:
        - "/src/go.mod"
        - "/src/go.sum"
        - "/src/core/**"
        - "/src/lib/**"
        - "/src/common/**"
        - "/src/pkg/**"
        - "/src/controller/**"
        - "/src/server/**"
    dependency_sources:
      - "/src/vendor"
    asset_context:
      asset_criticality: tier_1
      network_exposure: internal
      data_classification: confidential
      lifecycle: production
      regulatory_scope: []
    deployment: |
      The service that takes user and client requests. Reached from the private
      network through the nginx proxy container, which is the only container
      publishing host ports; core itself publishes none and sits on the harbor
      bridge network (make/photon/prepare/templates/docker_compose/
      docker-compose.yml.jinja).

      ATTACKER-CONTROLLED INPUT: HTTP requests from authenticated users and
      registry clients -- API parameters, headers, bearer and OIDC tokens,
      project and repository names, labels and descriptions.

      This is where the entire identity stack lives, and none of it is linked
      into any other service: coreos/go-oidc, go-jose, go-ldap, go-asn1-ber,
      Azure/go-ntlmssp, fxamacker/cbor. A vulnerability in any of those reaches
      only this context, and reaches it on the request path.

  - id: jobservice
    applies_to: >-
      Go dependencies linked into the jobservice binary (/src/jobservice). 140
      third-party modules at the reviewed commit. Shares /src/go.mod with the
      other four Go contexts -- see the note above.
    frameworks:
      - name: gocraft/work
        roles: [background-tasks]
      - name: robfig/cron
        roles: [scheduled-jobs]
    paths:
      include:
        - "/src/go.mod"
        - "/src/go.sum"
        - "/src/jobservice/**"
    dependency_sources:
      - "/src/vendor"
    asset_context:
      asset_criticality: tier_1
      network_exposure: isolated
      data_classification: confidential
      lifecycle: production
    deployment: |
      Runs background jobs off a Redis queue. Publishes no host port and is not
      proxied to users, so it is not INBOUND-routable -- hence isolated.

      DO NOT READ "isolated" AS "LOW RISK" HERE. network_exposure describes
      inbound reachability only, and this service's exposure is OUTBOUND: it is
      the HTTP client that performs replication pulls from remote registries,
      delivers webhooks to configured endpoints, and fetches proxy-cache
      upstreams. Those endpoints are configured by project admins, and because
      project_creation_restriction is left at "everyone", any authenticated
      user can become one. A dependency flaw reached by parsing a remote
      server's response, or by following its redirects, is reachable here.

      ATTACKER-CONTROLLED INPUT: artifact content it processes (manifests,
      layer blobs, chart metadata), responses from remote registries and
      webhook endpoints, and job parameters.

      VERIFIED, AND CONTRARY TO THE OBVIOUS ASSUMPTION: stretchr/testify is
      linked into the shipped jobservice binary -- /src/jobservice/period and
      /src/jobservice/mgt import it from non-test files. A testify finding
      binds to this production context, NOT to build tooling.

  - id: registryctl
    applies_to: >-
      Go dependencies linked into the registryctl binary (/src/registryctl). 95
      third-party modules at the reviewed commit -- the smallest surface of the
      network-facing services. Shares /src/go.mod -- see the note above.
    paths:
      include:
        - "/src/go.mod"
        - "/src/go.sum"
        - "/src/registryctl/**"
    dependency_sources:
      - "/src/vendor"
    asset_context:
      asset_criticality: tier_1
      network_exposure: isolated
      data_classification: confidential
      lifecycle: production
    deployment: |
      Control API sitting beside the docker registry container, called by core
      for garbage collection and blob deletion. Publishes no host port and is
      not proxied to users; callers authenticate with a shared secret.

      ATTACKER-CONTROLLED INPUT: none directly from users. Requests arrive from
      core over the bridge network, so reaching this service requires first
      compromising core or the bridge network.

  - id: exporter
    applies_to: >-
      Go dependencies linked into the exporter binary (/src/cmd/exporter). 121
      third-party modules at the reviewed commit. Shares /src/go.mod -- see the
      note above.
    paths:
      include:
        - "/src/go.mod"
        - "/src/go.sum"
        - "/src/cmd/exporter/**"
    dependency_sources:
      - "/src/vendor"
    asset_context:
      asset_criticality: tier_1
      network_exposure: internal
      data_classification: confidential
      lifecycle: production
    deployment: |
      Prometheus metrics endpoint. Metrics are ENABLED in this deployment, so
      the proxy publishes port 9090 and the exporter answers requests from the
      private network -- the same inbound exposure as core. (With metrics off,
      which is the shipped default, nothing would route to it.)

      ATTACKER-CONTROLLED INPUT: HTTP requests to the metrics endpoint from the
      private network. The exporter reads the Harbor database to build its
      responses.

  - id: db-migrator
    applies_to: >-
      Go dependencies linked into the standalone db-migrator binary
      (/src/cmd/standalone-db-migrator). 68 third-party modules at the reviewed
      commit -- the narrowest surface in the deployment. Shares /src/go.mod --
      see the note above.
    frameworks:
      - name: golang-migrate
        roles: [cli]
    paths:
      include:
        - "/src/go.mod"
        - "/src/go.sum"
        - "/src/cmd/standalone-db-migrator/**"
    dependency_sources:
      - "/src/vendor"
    asset_context:
      asset_criticality: tier_1
      network_exposure: isolated
      data_classification: confidential
      lifecycle: production
    deployment: |
      Runs schema migrations against Postgres, once, during an upgrade. It is
      not a long-running service: it has no listener, serves no requests, and
      is not running at all during normal operation.

      ATTACKER-CONTROLLED INPUT: none. Its inputs are the migration files
      shipped in the image and the database connection string. A finding whose
      only binding is this context is reached by an operator running an
      upgrade, not by an attacker -- which is the distinction this context
      exists to make.

  - id: portal
    applies_to: >-
      npm dependencies delivered to users' browsers: the Angular portal and the
      embedded Swagger UI. The portal image ships only the compiled bundle
      (make/photon/portal/Dockerfile copies dist/ into nginx). IMPORTANT: the
      same package.json also declares the Angular CLI, webpack and karma
      toolchain, which runs only during the image build and never ships -- no
      path can separate the two, so an npm alert binds here whether it affects
      browser-delivered code or build tooling only. Check which before scoring.
    frameworks:
      - name: Angular
        roles: [browser-ui]
      - name: Clarity
        roles: [browser-ui]
      - name: swagger-ui
        roles: [browser-ui]
      - name: Angular CLI / webpack
        roles: [build]
    paths:
      include:
        - "/src/portal/package.json"
        - "/src/portal/package-lock.json"
        - "/src/portal/app-swagger-ui/package.json"
        - "/src/portal/app-swagger-ui/package-lock.json"
        - "/src/portal/src/**"
    dependency_sources:
      - "/src/portal/node_modules"
      - "/src/portal/app-swagger-ui/node_modules"
    asset_context:
      asset_criticality: tier_1
      network_exposure: internal
      data_classification: confidential
      lifecycle: production
    deployment: |
      Served by nginx from the portal container to authenticated users on the
      private network. Code running here executes in a logged-in user's session
      against a tier_1 registry, so session or credential theft here reaches
      the registry itself.

      ATTACKER-CONTROLLED INPUT: the portal renders markdown that arrives
      inside pushed artifacts -- Helm chart README and values -- via
      ngx-markdown/marked/prismjs, see
      src/portal/src/app/shared/shared.module.ts:163. Anyone who can push an
      artifact is untrusted, so that markup is attacker-controlled.

      NOTE: neither node_modules directory is installed in this checkout (both
      are gitignored). Run `npm ci` in each before any analysis that needs to
      read a package's source.

  - id: build
    applies_to: >-
      GitHub Actions used only in continuous integration. These never run in
      the deployed service. Dependabot tracks this ecosystem at the repository
      root, so alerts report .github/workflows paths.
    paths:
      include:
        - "/.github/workflows/**"
    asset_context:
      asset_criticality: tier_2
      network_exposure: isolated
      lifecycle: dev
    deployment: |
      GitHub-hosted runners. Not inbound-routable, but they do have outbound
      network egress. Verifiable facts about what runs here:

        - .github/workflows/CI.yml triggers on pull_request, so code from an
          outside contributor's pull request executes on a runner. That is not
          a boundary this deployment defends -- see out_of_scope_actors.
        - .github/workflows/build-package.yml configures AWS credentials and
          runs on pushes to main and release branches.
        - .github/workflows/publish_release.yml runs on version tags with
          contents:write, id-token:write and packages:write, and performs
          Cosign keyless release signing.

      Rated tier_2: a compromise here reaches release artifacts and the signing
      identity, which hurts, but holds no customer data and a bad release can
      be pulled.

# ---------------------------------------------------------------------------
# The asset_criticality above is tier_1 for every runtime service because a
# compromise of any of them reaches the registry: they all hold database or
# artifact-storage access. What differs between them is inbound exposure and
# what input an attacker controls, which is stated per context rather than
# flattened into the criticality.
# ---------------------------------------------------------------------------

trust_model:
  untrusted_actors:
    - id: authenticated-users-other-projects
      description: >-
        Authenticated Harbor users with access to some projects but not others.
        The boundary between projects is defended. They control API parameters,
        project and repository names, labels and descriptions -- and, because
        project_creation_restriction is "everyone", the webhook, replication
        and proxy-cache URLs that a project admin may configure.
    - id: artifact-pushers
      description: >-
        Anyone who can push an image, chart or OCI artifact to a project.
        Artifact content is untrusted input: manifests, layer blobs,
        annotations, and the chart README and values markdown that the portal
        renders in another user's authenticated session.
    - id: internal-network-unauthenticated
      description: >-
        Anyone on the private network with no Harbor login. The login boundary
        is defended against them, not only the project boundary.
  trusted_actors:
    - id: system-administrators
      description: >-
        Harbor system administrators, who configure authentication, scanning
        and deployment settings. INFERRED from not being named as an untrusted
        actor -- confirm this is what you meant.
  out_of_scope_actors:
    - id: unauthenticated-internet
      description: >-
        Not routable from the internet; Harbor is reachable only from the
        private network.
    - id: fork-pull-request-contributors
      description: >-
        Outside contributors whose pull requests execute on CI runners
        (.github/workflows/CI.yml triggers on pull_request). Explicitly not a
        boundary defended here.

accepted_risks: []

# ---------------------------------------------------------------------------
# STATED CONFIGURATION OF THIS DEPLOYMENT
#
# Operator settings. They live in harbor.yml or Helm values, not in this
# repository, so the evidence for them is the operator's statement rather than
# a file here. SECURITY.md notes that Harbor's shipped defaults are not
# considered secure-by-default; these lines record what this deployment
# actually does about that. That upstream note is a report-triage policy, not a
# risk accepted here; accepted_risks is empty on purpose.
#
# Changed from the shipped defaults:
#   - Admin and Postgres passwords are generated secrets at install. The
#     shipped Harbor12345 / root123 are NOT in use.
#   - Clients reach Harbor over HTTPS with operator-supplied certificates.
#   - internal_tls is enabled and Postgres connections are encrypted, rather
#     than the shipped commented-out internal_tls and sslmode=disable.
#   - Trivy scanning is enabled (shipped default: WithTrivy=false).
#   - Metrics are enabled (shipped default: MetricEnable=false).
#
# Authentication:
#   - OIDC. UAA is not used, so the UAAVerifyCert=false default does not
#     apply. OIDCVerifyCert defaults to true.
#
# Shipped defaults deliberately carried:
#   - project_creation_restriction = "everyone"
#     (src/lib/config/metadata/metadatalist.go:115). Any authenticated user can
#     create a project and hold project-admin rights in it. CONSEQUENCE FOR
#     REACHABILITY: "project admin" is not a smaller group than "authenticated
#     user" here, so any capability granted to project admins, notably the
#     webhook, replication and proxy-cache URLs that jobservice fetches, is
#     available to any authenticated user.
#   - robot account token lifetime = 30 days
#     (src/lib/config/metadata/metadatalist.go:154).
#   - trace.enabled = false. Distributed tracing is OFF in this deployment
#     (TRACE_ENABLED, src/lib/config/metadata/metadatalist.go:166; the trace:
#     block ships commented out in make/harbor.yml.tmpl:270).
#     CONSEQUENCE FOR REACHABILITY: the OpenTelemetry Baggage propagator is
#     installed only when tracing initializes
#     (src/lib/trace/trace.go:128), and the HTTP trace middleware is a
#     pass-through when it is off (src/server/middleware/trace/trace.go:23),
#     so no inbound baggage header is parsed by any binary here.
# ---------------------------------------------------------------------------
