Parent runbook: docs/slo/completed/RUNBOOK-hulumi-k8s.md. Read the runbook's Global Execution Rules + Global Entry Rules + M2's lessons before starting.
Goal: After M3, @hulumi/k8s-baseline.AlbMeshedHttpEntrypoint ships as a pulumi.ComponentResource that emits four resources for one workload: a K8s Ingress (ALB target-type=ip, scheme, group.name, healthcheck-port=15021, healthcheck-path=/healthz/ready), an Istio Gateway in the istio-system namespace, an Istio VirtualService in the workload namespace cross-referencing the gateway, and an AuthorizationPolicy in the workload namespace with from.principals matching the ingress gateway's ServiceAccount principal — automatically wired from the IstioFoundation ref. Closes #41.
Context: The four-resource bundle is what every meshed workload re-derives by hand. The principal-name linkage between AuthorizationPolicy.from.principals and the gateway's SA is the bug class that the issue's repro flagged (~90 minutes per workload to debug); this milestone's load-bearing value is collapsing that linkage to a single mesh: IstioFoundation ref. The ALB-specific bits (target-type=ip, scheme, healthcheck on the gateway-native readiness path) are well-understood and stable across ALB Controller versions.
Important design rule: The AuthorizationPolicy.from.principals array is computed from mesh.ingressGatewayServiceAccountName, never from a consumer-supplied string. Hand-typing the principal is the bug source the abstraction exists to prevent. The extraPrincipals arg is allowed for additional SPIFFE IDs (e.g., a sister-service that should also be allowed in), but the gateway principal itself is non-overridable and is asserted by the principal_linkage_load_bearing BDD row.
Refactor budget: Surgical addition only. New packages/k8s-baseline/src/alb-meshed-http-entrypoint* files + tests. Modifies packages/k8s-baseline/src/index.ts (re-export), docs/slo/completed/RUNBOOK-hulumi-k8s.md Milestone Tracker.
| Field | Value |
|---|---|
| Inputs | new AlbMeshedHttpEntrypoint(name, args) where args: AlbMeshedHttpEntrypointArgs requires: mesh: IstioFoundation (concrete type at v1; the four output names are read from this ref), host: string (FQDN; refused if it doesn't match a basic FQDN regex), serviceRef: { namespace: string; name: string; port: number } (workload service to route to). Optional: scheme?: "internal" | "internet-facing" (default "internal"), mTLS?: "STRICT" | "PERMISSIVE" (default falls back to mesh's defaultMTLS at construction time), authorizationPolicy?: { allowFromGateway?: boolean; extraPrincipals?: string[] } (default { allowFromGateway: true, extraPrincipals: [] }), alb?: { healthcheckPath?: string; healthcheckPort?: number; groupName?: string; certificateArn?: string; sslPolicy?: string } (defaults: /healthz/ready, 15021, "default", no cert/policy). |
| Outputs | AlbMeshedHttpEntrypointOutputs exposes ingressName, ingressNamespace, gatewayName, gatewayNamespace, virtualServiceName, virtualServiceNamespace, authorizationPolicyName, authorizationPolicyNamespace (each pulumi.Output<string>), albAddress: pulumi.Output<string> (computed from the Ingress status — eventual, may be empty until ALB Controller provisions). |
| Interfaces touched | New stable surface: @hulumi/k8s-baseline#{AlbMeshedHttpEntrypoint,AlbMeshedHttpEntrypointArgs,AlbMeshedHttpEntrypointOutputs,ALB_MESHED_HTTP_ENTRYPOINT_COMPONENT_TYPE}. Component type: "hulumi:k8s:AlbMeshedHttpEntrypoint". Consumes IstioFoundation from M2. |
| Data classification | Internal — provisions K8s manifests in a kind cluster (or sandbox EKS) during integration tests. The component does not handle workload data at runtime; it shapes how the ALB and the mesh route it. |
| Proactive controls in play | (a) C1 Define Security Requirements — design record + the issue (#41) record the requirement: bundle the four resources with consistent SA-principal linkage. (b) C5 Validate All Inputs — host regex-validated; serviceRef requires non-empty namespace/name and a positive port; scheme and mTLS constrained to enums. (c) C7 Authorization — AuthorizationPolicy with from.principals matching the gateway SA is the security control; consumer's extraPrincipals are appended, never substituted. (d) C9 Implement Security Logging and Monitoring — emits pulumi.log.warn if authorizationPolicy.allowFromGateway: false (loud opt-out on the security-positive default — equivalent to "no AuthZ in front of this entrypoint"). (e) C10 Handle All Errors and Exceptions — refuses construction with clear errors for invalid host / serviceRef / mTLS / scheme. |
| Abuse acceptance scenarios | Four BDD rows in the table below cite tm-hulumi-k8s-abuse-N. Slug-keyed: tm-hulumi-k8s-abuse-principal-linkage-broken (asserts from.principals is computed from the mesh ref, not a consumer string), tm-hulumi-k8s-abuse-allow-everyone (allowFromGateway: false with no extraPrincipals is a no-AuthZ posture; refuses construction unless the consumer explicitly opts in via a separate acknowledgeNoAuthZ: true flag — borrowed from SecureRepository's public-visibility opt-in pattern), tm-hulumi-k8s-abuse-host-regex-bypass (refuses host: "*" and host: ".internal" and host: ""), tm-hulumi-k8s-abuse-cross-ns-virtualservice (VirtualService references the gateway via <istio-system-ns>/<gateway-name> cross-namespace form; this is the standard Istio shape — the test asserts the full ref, not a bare name). |
| Files allowed to change | New: packages/k8s-baseline/src/{alb-meshed-http-entrypoint.ts,alb-meshed-http-entrypoint.args.ts,alb-meshed-http-entrypoint.outputs.ts}; packages/k8s-baseline/tests/alb-meshed-http-entrypoint.test.ts; packages/k8s-baseline/tests/integration/kind/alb-meshed-http-entrypoint.kind.test.ts (kind cannot fully exercise ALB; the kind test asserts the four K8s manifests render correctly — ALB-side assertions live in the M5 real-EKS smoke); docs/slo/lessons/hulumi-k8s-m3.md; docs/slo/completion/hulumi-k8s-m3.md; docs/components/alb-meshed-http-entrypoint.md (one-line stub). Modified: packages/k8s-baseline/src/index.ts (re-export); docs/slo/completed/RUNBOOK-hulumi-k8s.md Milestone Tracker. Files outside this milestone's allow-list — REFUSE TO TOUCH including any packages/baseline/, packages/policies/, packages/drift/, M1 + M2 K8s files. |
| Files to read before changing anything | docs/slo/completed/RUNBOOK-hulumi-k8s.md; docs/slo/design/hulumi-k8s-surface.md (§ Decision: MeshedHttpEntrypoint — ALB-first); docs/slo/runbook-milestones/hulumi-k8s-m2.md; docs/slo/lessons/hulumi-k8s-m2.md; packages/k8s-baseline/src/istio-foundation.ts and .outputs.ts; AWS Load Balancer Controller's IngressClass and annotation reference. |
| New files allowed | All "New" entries above. |
| New dependencies allowed | none. |
| Migration allowed | no. |
| Compatibility commitments | AlbMeshedHttpEntrypoint, args, outputs, component-type constant — stable from M3. The shape of serviceRef ({ namespace, name, port }) is load-bearing; renaming any field is a breaking change. |
| Forbidden shortcuts | (a) NEVER allow from.principals to be set from a consumer string. The mesh ref's SA name is the single source of truth. (b) NEVER silently default allowFromGateway: false — the constructor refuses unless the consumer opts in via authorizationPolicy: { allowFromGateway: false, acknowledgeNoAuthZ: true }. (c) NEVER ship the ALB healthcheck pointing at the workload service (port 80, path /) as a default — the gateway-native readiness on port 15021 is the documented correct path. (d) NEVER allow scheme: "internet-facing" to be the default; the default is "internal" because the dominant deployment shape on EKS is internal-only. Internet-facing is an explicit opt-in. (e) NEVER infer the gateway's namespace from a "well-known" string; always read mesh.ingressGatewayNamespace (added to IstioFoundationOutputs if not already exposed — verify in M2 review). |
- No NLB / Cloudflare Tunnel / NodePort / nginx-ingress / traefik bundles. ALB only at v1 (Rule 0).
- No mTLS-PERMISSIVE-on-the-entrypoint convenience; the entrypoint inherits mesh-level mTLS by default.
- No multi-host fan-out (one VirtualService routing N hosts to N services). One entrypoint = one host = one service. Workloads needing fan-out compose multiple entrypoints.
- No automatic ACM certificate management.
alb.certificateArnis consumer-supplied. - No DNS record creation. Routes 53 / Cloudflare DNS is consumer-side.
- No WAF / Shield association. Consumer-side ALB attachment.
- No HTTP-to-HTTPS redirect annotation magic; if the consumer supplies a
certificateArn, the redirect annotation is added; otherwise not.
- Complete the Global Entry Rules.
- Read M2's lessons; particularly any surprises around the
ingressGatewayServiceAccountNameoutput computation. - Read the AWS Load Balancer Controller ingress annotation reference; the four annotations this milestone uses (
alb.ingress.kubernetes.io/target-type,…/scheme,…/healthcheck-port,…/healthcheck-path,…/group.name) are the load-bearing ones. - Copy the Evidence Log template.
- Re-state the load-bearing constraints: (i) principal linkage from mesh ref, never from consumer string; (ii)
allowFromGateway: falserequires explicitacknowledgeNoAuthZ: true; (iii) ALB healthcheck defaults to gateway-native (port 15021, path/healthz/ready); (iv)scheme: "internal"is the default. - Verify M2's
IstioFoundationOutputsexposesingressGatewayNamespace. If not, add it to M2 in a small follow-up before starting M3 (allowed because M2 outputs are load-bearing for M3 — bump it via a M2 patch + lesson).
| File | Planned Change |
|---|---|
packages/k8s-baseline/src/alb-meshed-http-entrypoint.ts |
NEW: ComponentResource emitting 4 children |
packages/k8s-baseline/src/alb-meshed-http-entrypoint.args.ts |
NEW |
packages/k8s-baseline/src/alb-meshed-http-entrypoint.outputs.ts |
NEW |
packages/k8s-baseline/tests/alb-meshed-http-entrypoint.test.ts |
NEW: BDD covering the 4-resource emission, principal linkage, abuse rows |
packages/k8s-baseline/tests/integration/kind/alb-meshed-http-entrypoint.kind.test.ts |
NEW: kind test asserting the 4 K8s objects render correctly (ALB itself is not exercised in kind) |
packages/k8s-baseline/src/index.ts |
MODIFY: re-export |
docs/components/alb-meshed-http-entrypoint.md |
NEW (one-line stub) |
docs/slo/completed/RUNBOOK-hulumi-k8s.md Milestone Tracker |
MODIFY |
docs/slo/runbook-milestones/hulumi-k8s-m3.md |
MODIFY (Evidence Log only, in execution) |
docs/slo/lessons/hulumi-k8s-m3.md |
NEW (during exit) |
docs/slo/completion/hulumi-k8s-m3.md |
NEW (during exit) |
- Verify M2's
IstioFoundationOutputsexposesingressGatewayNamespace; patch M2 if not. - Write
packages/k8s-baseline/tests/alb-meshed-http-entrypoint.test.tsBDD: happy path emits 4 children with correct kinds + namespaces; principal computed from mesh ref; missing acknowledgeNoAuthZ refused; invalid host refused; cross-ns VirtualService gateway ref correct; abuse rows. - Implement args, outputs, component. The constructor: validates inputs; reads
mesh.ingressGatewayServiceAccountNameandingressGatewayNamespace; emitskubernetes.networking.v1.Ingress(workload namespace, with the four ALB annotations + gateway service backend),kubernetes.apiextensions.CustomResourceforGateway(istio-system),VirtualService(workload namespace, gateway ref<istio-system>/<gateway-name>),AuthorizationPolicy(workload namespace, principals =[mesh-sa-principal, ...extraPrincipals]). - Wire re-export. Run
pnpm --filter @hulumi/k8s-baseline build && test && typecheck && lint— green. - Write the kind integration test asserting the four objects exist with correct shape; deploy a tiny workload service for the entrypoint to point at.
- Run full repo regression sweep. Update Tracker, lessons, completion.
Feature: AlbMeshedHttpEntrypoint emits ALB Ingress + Istio Gateway/VirtualService/AuthorizationPolicy with principal linkage automatically wired from the mesh ref
| Scenario | Category | Given | When | Then | Threat-model row | Control |
|---|---|---|---|---|---|---|
| Happy path — minimal args | happy path | mock-runtime; mesh is a constructed IstioFoundation; host: "api.example.internal"; serviceRef: { namespace: "prod", name: "api", port: 9090 } |
construction | 4 children registered: Ingress in prod (with annotations: target-type: ip, scheme: internal, healthcheck-port: "15021", healthcheck-path: /healthz/ready, group.name: default, backend service is the istio-ingressgateway); Gateway in istio-system (selector matches gateway pods, host api.example.internal); VirtualService in prod (gateways [istio-system/<gateway-name>], host api.example.internal); AuthorizationPolicy in prod (from.principals = [<gateway SA principal>]) |
n/a | n/a |
Happy path — extraPrincipals appended |
happy path | mock-runtime; authorizationPolicy: { allowFromGateway: true, extraPrincipals: ["spiffe://example.org/sa/sister-svc"] } |
construction | AuthorizationPolicy from.principals = [<gateway-sa-principal>, "spiffe://example.org/sa/sister-svc"] (gateway always first; extra appended) |
n/a | n/a |
Happy path — scheme: "internet-facing" opt-in |
happy path | mock-runtime; scheme: "internet-facing" |
construction | Ingress annotation scheme: internet-facing |
n/a | C5 |
| Invalid input — invalid host | invalid input | mock-runtime; host: "*.example" |
construction | constructor throws Error('AlbMeshedHttpEntrypoint: host must be a valid FQDN'); no children registered |
tm-hulumi-k8s-abuse-host-regex-bypass |
C5 |
| Invalid input — invalid serviceRef.port | invalid input | mock-runtime; serviceRef.port: -1 |
construction | constructor throws Error mentioning port must be 1-65535 |
n/a | C5 |
| Invalid input — invalid scheme | invalid input | mock-runtime; scheme: "external" (typo) |
construction | constructor throws Error mentioning enum |
n/a | C5 |
Empty state — mesh.ingressGateway: { enabled: false } |
empty state | mock-runtime; mesh constructed with ingressGateway: { enabled: false } |
construction | constructor throws Error('AlbMeshedHttpEntrypoint requires mesh.ingressGateway.enabled === true') |
n/a | C5 |
Dependency failure — kubernetes.Provider unconfigured |
partial failure | mock-runtime where the implicit kubernetes.Provider lacks credentials |
construction | mock-runtime test passes; kind test gracefully skips when binary absent | n/a | env-var-gating |
| Abuse case — principal linkage load-bearing | abuse case | mock-runtime; valid args | construction | the AuthorizationPolicy from.principals[0] is identity-equal to mesh.ingressGatewayServiceAccountName's SPIFFE form (asserted directly against the mesh output, not against a string literal) |
tm-hulumi-k8s-abuse-principal-linkage-broken |
Forbidden shortcut (a) |
Abuse case — allowFromGateway: false without acknowledgement refused |
abuse case | mock-runtime; authorizationPolicy: { allowFromGateway: false } (no acknowledgeNoAuthZ) |
construction | constructor throws Error('AlbMeshedHttpEntrypoint: allowFromGateway: false requires acknowledgeNoAuthZ: true and a non-empty extraPrincipals list') |
tm-hulumi-k8s-abuse-allow-everyone |
Forbidden shortcut (b) |
| Abuse case — host wildcard refused | abuse case | mock-runtime; host: "*" |
construction | constructor throws — no AuthorizationPolicy without a real host | tm-hulumi-k8s-abuse-host-regex-bypass |
C5 |
| Abuse case — VirtualService cross-namespace ref present | abuse case | mock-runtime; mesh in istio-system, workload in prod |
construction | VirtualService.spec.gateways = ["istio-system/<gateway-name>"] (cross-namespace form), NOT just ["<gateway-name>"] |
tm-hulumi-k8s-abuse-cross-ns-virtualservice |
n/a (correct-by-construction) |
| Compatibility — outputs lock | schema / compatibility | construction succeeds | inspect outputs | the 9 documented outputs are present (ingressName, ingressNamespace, gatewayName, gatewayNamespace, virtualServiceName, virtualServiceNamespace, authorizationPolicyName, authorizationPolicyNamespace, albAddress); none renamed |
n/a | type-layer schema lock |
- All M1 + M2 BDD scenarios continue to pass.
- AWS + GitHub regression suites continue to pass.
- Component + args + outputs + type constant exported.
-
pnpm install --frozen-lockfile && pnpm -r build && pnpm -r test && pnpm -r typecheck && pnpm -r lint && pnpm run lint:license-boundary && pnpm run lint:exact-pin-guardgreen. - License header on every new file.
-
serviceRef.{namespace,name,port}shape is load-bearing — documented indocs/components/alb-meshed-http-entrypoint.mdstub.
| E2E Test | What It Proves | Pass Criteria |
| ------------------------------------------------ | ------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------- |
| emits_four_objects_against_kind | The four-resource bundle renders correctly | Kind test: install M2 + this; kubectl get ingress,gateway,virtualservice,authorizationpolicy -A returns the 4 expected objects with correct names |
| principal_present_in_authorization_policy_yaml | The principal linkage actually reaches the rendered manifest | kubectl get authorizationpolicy <name> -n <ns> -o yaml | grep principals shows the gateway SA principal |
| cross_namespace_gateway_ref_present | The cross-ns ref is correctly rendered | kubectl get virtualservice <name> -n <ns> -o yaml | yq '.spec.gateways[0]'returnsistio-system/<gateway-name> |
- Full sweep green.
- Kind test green or skipped cleanly.
- In a Pulumi program:
new AlbMeshedHttpEntrypoint("e", { mesh, host: "*", serviceRef: { ... } } as any)causes preview to fail with the host-regex error. -
git statusclean.
| Step | Command / Check | Expected Result | Actual Result | Pass/Fail | Notes |
|---|---|---|---|---|---|
| Baseline | pnpm -r build && pnpm -r test |
green pre-M3 | filled during execution | pending | |
| M2 outputs verification | grep ingressGatewayNamespace in istio-foundation.outputs.ts |
present (or patched in this milestone) | filled during execution | pending | |
| BDD tests created | pnpm --filter @hulumi/k8s-baseline test -- alb-meshed-http-entrypoint |
fail for module-not-found | filled during execution | pending | |
| Component impl | filesystem | source files present + re-exported | filled during execution | pending | |
| Mock-runtime BDD | pnpm --filter @hulumi/k8s-baseline test |
all rows pass | filled during execution | pending | |
| Build / typecheck / lint | pnpm -r build && pnpm -r typecheck && pnpm -r lint |
green | filled during execution | pending | |
| Kind integration | KIND_E2E=1 pnpm --filter @hulumi/k8s-baseline test:integration:kind |
green or skipped | filled during execution | pending | |
| Smoke | full sweep | green | filled during execution | pending |
Same as M2 standard. + docs/issue-candidates.md strikes #41.
- Tracker M3 →
done. docs/components/alb-meshed-http-entrypoint.mdstub.docs/issue-candidates.md— strike #41.
- The
mesh.ingressGatewayServiceAccountName↔from.principalslinkage is the abstraction's load-bearing contract. - ALB-side runtime behavior (target health, connection draining, idle timeout) is NOT exercised in kind; M5's real-EKS smoke covers it.