Skip to content

feat: Phase 3 — hosting rendered maps under their own address - #20

Open
TheMeinerLP wants to merge 2 commits into
clean2/phase-2b-ingestfrom
clean2/phase-3-hosting
Open

feat: Phase 3 — hosting rendered maps under their own address#20
TheMeinerLP wants to merge 2 commits into
clean2/phase-2b-ingestfrom
clean2/phase-3-hosting

Conversation

@TheMeinerLP

Copy link
Copy Markdown
Contributor

Note: This replaces #12, stacked on clean2/phase-2b-ingest instead of clean/phase-2b-ingest, and squashed on a base where the hosting entrypoint's environment-variable validation already uses the require_env helper, so the squash commit itself never contains the shell idiom the scanner misread as a secret.

Replaces #4, closed. Same content, rebuilt as a single squash commit on clean/phase-3-hosting because a secret scanner flagged disposable test credentials in the old commit history and history cannot be rewritten in this environment. See the closing comment on #4 for detail.

Phase 3 of Apus: hosting. Stacked on PR #3 (phase 2b), which must land first.

A rendered map now becomes reachable under its own address. A BlueMapHosting resource
produces a BlueMap webserver Deployment that reads the finished maps straight from S3,
plus Service, Ingress and — where cert-manager is present — a Certificate, and reports
the URL back in status.

What this delivers

Component Role
BlueMapHosting CRD maps to publish, hostname, ingress class, TLS, replicas
BlueMapConfigBuilder.buildForHosting multi-map config plus webserver.conf — the case the env-var contract cannot cover
hosting image BlueMap CLI in webserver mode with the S3 storage addon
HostingResourceBuilder Deployment, Service, Ingress, Certificate — all owned by the hosting resource
BlueMapHostingReconciler wires it together, enforces tenant isolation, restarts pods on config change

BlueMapConfigBuilder was built in phase 2a, found to be pointless for the render path,
and deliberately kept unwired with a javadoc note saying phase 3 would need it. That is
exactly what happened.

Two multi-tenancy holes, closed here

Both were reported by a security review of the merged builder code.

Hostnames were never checked against the tenant's allowed domains. Tenant.spec.hosting.allowedDomains
was documented in the spec but modelled nowhere and enforced by nobody — so a tenant could
have claimed another tenant's hostname and taken over its traffic. The reconciler now
resolves the owning tenant via the namespace label, matches the hostname (literal or
single-level wildcard, mirroring wildcard-TLS semantics) and refuses with a condition
instead of creating an Ingress. An empty domain list means "not configured", not
"unrestricted".

Referenced maps were not confined to the hosting's own namespace. A hosting could have
published another tenant's maps. Maps are now resolved only within the hosting's namespace;
a missing one is an error, not a hint to look elsewhere.

Bugs the real cluster found that mocks could not

  • ConfigMap data keys may not contain /. The generated config used logical keys like
    maps/survival-overworld.conf; the mock server accepted them, a real API server rejects
    them outright. Keys are now sanitised, with the nested path restored through the volume's
    items[].path, where slashes are legal. Without the cluster test this would have broken
    every hosting in production.
  • Certificate.CertificateStatus was a genuinely empty class, which crashes Jackson on
    any real client write. Invisible while only the pure builder was under test.
  • -w alone serves 404 for everything until -g has run once — found by actually
    running the image, not by reading flags.

Testing

204 operator unit tests, plus 8 integration tests against real k3s. The
client.supports(Certificate.class) branch is now genuinely exercised: on a cert-manager-less
cluster it returns false and the reconciler blocks the whole hosting rather than silently
skipping TLS. That branch was previously untestable — the mock server always answers true.

Explicitly not covered

  • No real Ingress controller, so no HTTP request travelling through one. Task 2 did prove a
    hosting pod serves real tiles over HTTP via a direct call.
  • The cert-manager-present path (an actual Certificate being issued) is still mock-only.
  • Rook stays faked via BlueMapMap.status.bucket, as in the existing operator integration test.
  • No separate whole-phase review ran; the findings above came from the per-task reviews and
    the background security review.

Hosting layer on top of Phase 2b. A rendered map now becomes reachable
under its own address. A BlueMapHosting resource produces a BlueMap
webserver Deployment that reads the finished maps straight from S3, plus
Service, Ingress and — where cert-manager is present — a Certificate, and
reports the URL back in status.

- BlueMapHosting CRD (maps to publish, hostname, ingress class, TLS,
  replicas); BlueMapConfigBuilder.buildForHosting for the multi-map config
  plus webserver.conf; a hosting image running the BlueMap CLI in
  webserver mode with the S3 storage addon, whose entrypoint validates its
  required environment variables with the same single-mention helper
  function as the Phase 1 runner image; HostingResourceBuilder for the
  owned Deployment/Service/Ingress/Certificate; BlueMapHostingReconciler
  wiring it together, enforcing tenant isolation, restarting pods on
  config change.
- Closes two multi-tenancy holes found in security review: hostnames were
  never checked against Tenant.spec.hosting.allowedDomains, so a tenant
  could claim another tenant's hostname; and referenced maps were not
  confined to the hosting's own namespace, so a hosting could have
  published another tenant's maps. Both are now enforced with a condition
  instead of silently proceeding.
- Fixes bugs only a real cluster surfaced: ConfigMap data keys may not
  contain `/` (generated keys are now sanitised, nesting restored via the
  volume's items[].path); Certificate.CertificateStatus was an empty class
  that crashed Jackson on any real client write; `-w` alone serves 404 for
  everything until `-g` has run once.
- 204 operator unit tests, plus 8 integration tests against real k3s,
  including the client.supports(Certificate.class) branch that the mock
  server could never exercise.

This branch replaces clean/phase-3-hosting (PR #12), stacked on
clean2/phase-2b-ingest instead of clean/phase-2b-ingest, and squashed on a
base where the hosting entrypoint's environment-variable validation already
uses the helper function, so no follow-up fix commit is needed to clear the
scanner finding.
@gitguardian

gitguardian Bot commented Aug 9, 2026

Copy link
Copy Markdown

️✅ There are no secrets present in this pull request anymore.

If these secrets were true positive and are still valid, we highly recommend you to revoke them.
While these secrets were previously flagged, we no longer have a reference to the
specific commits where they were detected. Once a secret has been leaked into a git
repository, you should consider it compromised, even if it was deleted immediately.
Find here more information about risks.


🦉 GitGuardian detects secrets in your source code to help developers and security teams secure the modern development process. You are seeing this because you or someone else with access to this repository has authorized GitGuardian to scan your pull request.

@TheMeinerLP
TheMeinerLP force-pushed the clean2/phase-2b-ingest branch from 98276bd to e31fa4a Compare August 9, 2026 10:30
@TheMeinerLP
TheMeinerLP force-pushed the clean2/phase-3-hosting branch from cb571e5 to 0a64b11 Compare August 9, 2026 10:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant