A nullplatform service that manages dynamic exposure of application endpoints through public and private domains. It translates high-level route declarations into native Kubernetes resources using Istio — HTTPRoutes, AuthorizationPolicies, and RequestAuthentication — all without developers needing to touch YAML.
Developers declare which HTTP routes they want to expose, which nullplatform scope backs each route, and which user groups are allowed to call it. The service handles the rest:
- Creates HTTPRoutes (Kubernetes Gateway API v1) pointing to the right backend service
- Creates AuthorizationPolicies enforcing group-based access control
- Creates RequestAuthentication resources validating JWT tokens (Cognito) or delegating to AVP
Route visibility is resolved automatically from the scope's own visibility attribute (external → public gateway, internal → private gateway).
AUTH_TYPE |
Mechanism |
|---|---|
aws-cognito |
Istio validates Cognito JWT; AuthorizationPolicies check cognito:groups claims |
aws-avp |
Amazon Verified Permissions policy store controls access |
When creating or updating the service, developers configure one or more routes:
| Field | Description |
|---|---|
| Verbs | HTTP methods (GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS) |
| Path | Route path. Supports exact (/api/users), parameterized (/api/users/{id}), and wildcard (/api/users/*) |
| Scope | nullplatform scope slug that backs this route |
| Authorized Groups | Comma-separated list of groups allowed to call this route (e.g. admin, read-only) |
Auth configuration is not part of the developer UI — it is set once at the infrastructure level via agent environment variables (see below).
The HCL in this section is illustrative, not a module to reference directly. Every snippet below — including
specs/install/andspecs/requirements/aws/— exists in this repository as a working reference implementation (and test fixture), not as a published module. Don't addsource = "git::https://github.com/nullplatform/services-endpoint-exposer.git//specs/install?ref=..."(or//specs/requirements/aws) to your own project. Copy the underlyingnullplatform/tofu-modulesmodule calls shown below into your project's own.tffiles and adapt the values (nrn,tags_selectors, refs, etc.) instead. This keeps your project's module versions, api keys, and scope wiring under your own control instead of an indirect reference to this repo.
- A running nullplatform agent with
kubectlaccess to the cluster - Istio installed with Gateway API CRDs
gateway-publicandgateway-privateGateway resources deployed
The following two module calls (copied from specs/install/main.tf — read that file for the definitive, up-to-date version) register this repo's service spec/entrypoint with nullplatform and wire a notification channel so an agent picks up the service's own create/update/delete/link actions:
module "service_definition_endpoint_exposer" {
source = "git::https://github.com/nullplatform/tofu-modules.git//nullplatform/service_definition?ref=<tofu-modules version>"
nrn = var.nrn
repository_org = "nullplatform"
repository_name = "services-endpoint-exposer"
repository_branch = "main" # or the branch you're testing
service_path = "" # specs live at repo root
service_name = "Endpoint Exposer"
available_links = ["connect"]
}
module "service_definition_channel_association_endpoint_exposer" {
source = "git::https://github.com/nullplatform/tofu-modules.git//nullplatform/service_definition_agent_association?ref=<tofu-modules version>"
nrn = var.nrn
api_key = var.np_api_key
tags_selectors = { "owner" = "my-agent", "environment" = "{$context.service.dimensions.environment}" }
service_specification_slug = module.service_definition_endpoint_exposer.service_specification_slug
repository_service_spec_repo = "nullplatform/services-endpoint-exposer"
service_path = "" # entrypoint lives at repo root
}Pin <tofu-modules version> to a real tag from nullplatform/tofu-modules releases — check its CHANGELOG.md for breaking changes before bumping.
Auth configuration is resolved at runtime from the agent's environment, not from the developer UI. Set these variables in the agent's extra_envs (Helm) or equivalent.
| Variable | Description | Example |
|---|---|---|
AUTH_TYPE |
Authorization scheme for the entire installation | aws-cognito |
INGRESS_TYPE |
Must be istio |
istio |
One variable per nullplatform environment dimension value (uppercased):
| Variable | Description | Example |
|---|---|---|
COGNITO_USER_POOL_ARN_<ENV> |
ARN of the Cognito User Pool for that environment | COGNITO_USER_POOL_ARN_PRODUCTION=arn:aws:cognito-idp:us-east-1:123456789:userpool/us-east-1_AbCdEf |
<ENV> corresponds to service.dimensions.environment uppercased (e.g. dev → DEV, production → PRODUCTION).
By default, Istio only looks for the Cognito JWT in the Authorization: Bearer <token> header (or an access_token query param). If the frontend instead sends the token as a cookie (e.g. id_token), set:
| Variable | Description | Example |
|---|---|---|
COGNITO_TOKEN_COOKIE_NAME |
Name of the cookie holding the Cognito id_token. When unset, falls back to the default header/query-param extraction. |
COGNITO_TOKEN_COOKIE_NAME=id_token |
This is global (applies to every environment's RequestAuthentication), not per-environment. Only the JWT itself (Cognito's id_token) can be validated this way — the refresh_token is an opaque token, not a JWT, and isn't usable here.
| Variable | Description | Example |
|---|---|---|
AVP_POLICY_STORE_ARN_<ENV> |
ARN of the Amazon Verified Permissions Policy Store | AVP_POLICY_STORE_ARN_PRODUCTION=arn:aws:verifiedpermissions::123456789:policy-store/AbCdEf |
OPA_PROVIDER_NAME |
Name of the OPA ext-authz provider in the cluster | opa-ext-authz |
With aws-avp, the service also calls the Amazon Verified Permissions API directly, so the agent needs AWS credentials for that. specs/requirements/aws is a reference implementation of the IAM role the agent assumes — copy its resources into your own project once per cluster rather than sourcing it from this repo (see the note at the top of this section), then pass the role output to the agent:
# Illustrative — read specs/requirements/aws/main.tf and copy its resources
# into your own project instead of sourcing this path directly.
module "endpoint_exposer_requirements" {
source = "git::https://github.com/nullplatform/services-endpoint-exposer.git//specs/requirements/aws?ref=main"
cluster_name = var.cluster_name
}aws-cognito makes no AWS API calls (Istio validates the JWT against Cognito's JWKS endpoint directly), so this module is not needed in that mode.
| Variable | Default | Description |
|---|---|---|
PUBLIC_GATEWAY_NAME |
gateway-public |
Name of the public Istio Gateway resource |
PRIVATE_GATEWAY_NAME |
gateway-private |
Name of the private Istio Gateway resource |
GATEWAY_NAMESPACE |
gateways |
Kubernetes namespace where Gateway resources live |
module "agent" {
source = "git::https://github.com/nullplatform/tofu-modules.git//nullplatform/agent?ref=<version>"
# ... other agent config ...
extra_envs = {
INGRESS_TYPE = "istio"
AUTH_TYPE = "aws-cognito"
COGNITO_USER_POOL_ARN_DEV = "arn:aws:cognito-idp:us-east-1:123456789:userpool/us-east-1_AbCdEf"
COGNITO_USER_POOL_ARN_PRODUCTION = "arn:aws:cognito-idp:us-east-1:123456789:userpool/us-east-1_XyZwVu"
}
}The container-scope-override/ directory injects a sync_exposer step into scope deploy workflows (initial, blue_green, switch_traffic, finalize, rollback, delete). This keeps HTTPRoutes in sync when the underlying Kubernetes service names change during a blue/green deploy.
This mechanism only activates when the scope agent entrypoint receives --overrides-path= pointing to the override directory. That flag must be set on the notification channel that fires for the target scope — i.e. the scope specification used by apps that expose routes through this service — not on the service channel from step 1 above.
enabled_override / override_repo_path / overrides_service_path are extra inputs on the standard nullplatform/tofu-modules//nullplatform/scope_definition_agent_association module — not a separate mechanism. A given scope specification should only ever have one scope_definition_agent_association module call. If your project already registers a notification channel for that scope (it almost always does — that's what makes the scope's own deploy/lifecycle actions work at all), add these three inputs to that same module call:
module "scope_definition_agent_association" {
source = "git::https://github.com/nullplatform/tofu-modules.git//nullplatform/scope_definition_agent_association?ref=<tofu-modules version>"
nrn = var.nrn
tags_selectors = var.tags_selectors
api_key = module.scope_definition_agent_association_api_key.api_key
scope_specification_id = var.scope_specification_id
scope_specification_slug = var.scope_specification_slug
# container-scope-override for services-endpoint-exposer
enabled_override = true
override_repo_path = "/root/.np/nullplatform/services-endpoint-exposer"
overrides_service_path = "/container-scope-override"
}Do not add a second
scope_definition_agent_associationcall for the same scope just to carry the override — e.g. by callingspecs/install's wrapper withenable_scope_channel = true(or writing your ownmodule "endpoint_exposer_scope_channel" { ... }) alongside a module that already registers that scope's channel. The channel'sfiltersare built solely fromscope_specification_slug/scope_specification_id;enabled_overridedoesn't change them, it only appends--overrides-path=...to the channel'scmdline. Two module calls pointed at the same scope produce twonullplatform_notification_channelresources with identical filters, so every action notification for that scope fires both channels — the scope's base entrypoint logic runs twice, once per channel, and one of the two invocations additionally triggers the override sync. Only register a brand-new, dedicated channel for the override when the target scope has no existing agent association in your project at all.
Without this override wired into the scope's channel, HTTPRoutes will point to stale backend service names after a blue/green deploy and traffic will break.
On every action (create / update / delete) the service:
- Reads
AUTH_TYPEfrom the agent environment - Reads
service.dimensions.environmentfrom the action context (e.g."dev") - Uppercases and normalizes the value →
DEV - Looks up
COGNITO_USER_POOL_ARN_DEV(orAVP_POLICY_STORE_ARN_DEV) via bash indirect expansion - Fails with a clear error if the required variable is not set
This means a single agent deployment can serve multiple environments, each with its own pool/store ARN.
├── entrypoint/ # Action handler (service, link)
├── scripts/
│ ├── common/ # apply, manage_policies
│ ├── istio/ # build_context, build_httproute, process_routes, build_allow_policies,
│ │ # build_request_authentication, delete_*, fetch_provider_data, config
│ ├── np/ # update_service_results
│ └── avp/ # AVP-specific policy management (aws-avp only)
├── specs/
│ ├── service-spec.json.tpl
│ ├── links/connect.json.tpl
│ ├── install/ # Reference OpenTofu implementation of the nullplatform registration (see "Installation with tofu modules" — illustrative, not meant to be sourced directly)
│ └── requirements/aws/ # Reference OpenTofu implementation of the AVP IAM role (aws-avp only — same caveat as install/)
├── templates/istio/ # Kubernetes resource templates (httproute, authorizationpolicy, request-authentication)
├── workflows/istio/ # create.yaml, update.yaml, delete.yaml, read.yaml
├── test/ # BATS test suite
└── container-scope-override/ # Deployment templates for override scope agent
./test/run-tests.shTests use BATS and cover HTTPRoute generation, AuthorizationPolicy creation, context building, and apply/cleanup flows.
# HTTPRoutes
kubectl get httproutes -n nullplatform
# AuthorizationPolicies
kubectl get authorizationpolicies -n gateways
# RequestAuthentication (Cognito JWT rules)
kubectl get requestauthentication -n gateways