From b760af974ea52d85728b2b21185112d02efd63f9 Mon Sep 17 00:00:00 2001 From: Javi Date: Wed, 14 Jan 2026 12:26:06 -0300 Subject: [PATCH 1/9] feat: add scope jwt token validation --- k8s/scope/build_context | 14 +- k8s/scope/security/README.md | 128 ++++++++++++++++++ k8s/scope/security/manage_jwt_auth | 118 ++++++++++++++++ .../templates/authorization-policy.yaml.tmpl | 28 ++++ .../request-authentication.yaml.tmpl | 27 ++++ k8s/scope/workflows/create.yaml | 8 ++ k8s/scope/workflows/delete.yaml | 8 ++ k8s/scope/workflows/update.yaml | 10 +- 8 files changed, 335 insertions(+), 6 deletions(-) create mode 100644 k8s/scope/security/README.md create mode 100755 k8s/scope/security/manage_jwt_auth create mode 100644 k8s/scope/security/templates/authorization-policy.yaml.tmpl create mode 100644 k8s/scope/security/templates/request-authentication.yaml.tmpl diff --git a/k8s/scope/build_context b/k8s/scope/build_context index e60aa4ae..1aac6ad0 100755 --- a/k8s/scope/build_context +++ b/k8s/scope/build_context @@ -37,14 +37,14 @@ REGION=$(echo "$CONTEXT" | jq -r '.providers["cloud-providers"].account.region / SCOPE_VISIBILITY=$(echo "$CONTEXT" | jq -r '.scope.capabilities.visibility') -if [ "$SCOPE_VISIBILITY" = "public" ]; then - DOMAIN=$(echo "$CONTEXT" | jq -r --arg default "$DOMAIN" ' - .providers["cloud-providers"].networking.domain_name // $default - ') -else +if [ "$SCOPE_VISIBILITY" = "internal" ]; then DOMAIN=$(echo "$CONTEXT" | jq -r --arg private_default "$PRIVATE_DOMAIN" --arg default "$DOMAIN" ' (.providers["cloud-providers"].networking.private_domain_name // $private_default | if . == "" then empty else . end) // .providers["cloud-providers"].networking.domain_name // $default ') +else + DOMAIN=$(echo "$CONTEXT" | jq -r --arg default "$DOMAIN" ' + .providers["cloud-providers"].networking.domain_name // $default + ') fi SCOPE_DOMAIN=$(echo "$CONTEXT" | jq .scope.domain -r) @@ -68,6 +68,10 @@ else export INGRESS_VISIBILITY="internal" GATEWAY_DEFAULT="${PRIVATE_GATEWAY_NAME:-gateway-internal}" export GATEWAY_NAME=$(echo "$CONTEXT" | jq -r --arg default "$GATEWAY_DEFAULT" '.providers["container-orchestration"].gateway.private_name // $default') +else + export INGRESS_VISIBILITY="internet-facing" + GATEWAY_DEFAULT="${PUBLIC_GATEWAY_NAME:-gateway-public}" + export GATEWAY_NAME=$(echo "$CONTEXT" | jq -r --arg default "$GATEWAY_DEFAULT" '.providers["container-orchestration"].gateway.public_name // $default') fi K8S_MODIFIERS="${K8S_MODIFIERS:-"{}"}" diff --git a/k8s/scope/security/README.md b/k8s/scope/security/README.md new file mode 100644 index 00000000..62e95b7b --- /dev/null +++ b/k8s/scope/security/README.md @@ -0,0 +1,128 @@ +# JWT Authentication for Scopes + +This directory contains scripts and templates for managing JWT authentication on scopes with `exposer` visibility. + +## Overview + +When a scope is created with `visibility: exposer`, the workflow automatically provisions JWT authentication resources in the Istio gateway namespace: + +1. **RequestAuthentication** - Validates JWT tokens from nullplatform scope API (one per cluster) +2. **AuthorizationPolicy** - Enforces JWT requirement per scope domain + +## How it Works + +### Create/Update Workflow + +1. The `manage_jwt_auth` script checks if `SCOPE_VISIBILITY` is "exposer" +2. If yes: + - Checks if `RequestAuthentication` exists globally (one per cluster) + - Creates it if it doesn't exist + - Creates/updates an `AuthorizationPolicy` specific to the scope's domain + +### Delete Workflow + +1. Deletes the scope-specific `AuthorizationPolicy` +2. Leaves the global `RequestAuthentication` intact (shared by all scopes) + +## Configuration + +The following environment variables are used: + +- `SCOPE_VISIBILITY` - Must be "exposer" to enable JWT auth +- `SCOPE_DOMAIN` - The domain to protect (e.g., `app.example.com`) +- `GATEWAY_NAME` - The Istio gateway name (e.g., `gateway-public`) +- `GATEWAY_NAMESPACE` - Namespace where gateway resources live (default: `gateways`) +- `JWT_ISSUER` - JWT issuer URL (default: `https://api.nullplatform.com/scope`) +- `JWT_JWKS_URI` - JWKS endpoint (default: `https://api.nullplatform.com/scope/.well-known/jwks.json`) + +## Resources Created + +### RequestAuthentication: `nullplatform-scope-jwt-auth` + +- **Location**: `gateways` namespace +- **Scope**: Cluster-wide (one instance) +- **Function**: Validates JWT signature and extracts claims +- **Behavior**: Does not enforce authentication (that's done by AuthorizationPolicy) + +### AuthorizationPolicy: `require-jwt-{scope-id}` + +- **Location**: `gateways` namespace +- **Scope**: Per scope domain +- **Function**: Denies access without valid JWT +- **Action**: DENY +- **Applies to**: Specific scope domain only +- **Exceptions**: Health endpoints (`/health`, `/ready`, `/metrics`) + +## JWT Token Requirements + +Tokens must: +- Be signed by `https://api.nullplatform.com/scope` +- Have valid signature (verified against JWKS) +- Include `iss` claim matching the issuer +- Include `aud` claim matching the target domain +- Not be expired + +## Token Sources + +The system accepts JWT tokens from: +1. `Authorization` header with `Bearer ` prefix +2. `np_scope_token` cookie + +## Extracted Claims + +The following JWT claims are extracted to HTTP headers: +- `sub` → `X-User-ID` +- `email` → `X-User-Email` +- `scope` → `X-User-Scopes` + +## Example + +For a scope with: +- ID: `playground-floppy-bird-api-production-kjstb` +- Domain: `playground-floppy-bird-api-production-kjstb.edenred.nullimplementation.com` +- Visibility: `exposer` + +The workflow creates: +1. `RequestAuthentication/nullplatform-scope-jwt-auth` (if not exists) +2. `AuthorizationPolicy/require-jwt-playground-floppy-bird-api-production-kjstb` + +Access behavior: +- ✅ Requests with valid JWT → Allowed +- ❌ Requests without JWT → 403 RBAC Denied +- ✅ Health endpoints → Always allowed + +## Files + +``` +scope/security/ +├── README.md # This file +├── manage_jwt_auth # Main script +└── templates/ + ├── request-authentication.yaml.tmpl # RequestAuthentication template + └── authorization-policy.yaml.tmpl # AuthorizationPolicy template +``` + +## Troubleshooting + +### JWT validation failing + +Check that: +1. Token is not expired +2. Token `aud` claim matches the domain being accessed +3. Token `iss` claim is `https://api.nullplatform.com/scope` +4. JWKS endpoint is accessible from the cluster + +### AuthorizationPolicy not working + +Verify: +1. Scope visibility is "exposer" +2. AuthorizationPolicy exists: `kubectl get authorizationpolicy -n gateways` +3. Policy selector matches gateway pods +4. Check Envoy logs: `kubectl logs -n gateways -f` + +### RequestAuthentication not created + +Check: +1. Template files exist in `scope/security/templates/` +2. Gomplate is installed and working +3. kubectl has permissions to create resources in gateways namespace diff --git a/k8s/scope/security/manage_jwt_auth b/k8s/scope/security/manage_jwt_auth new file mode 100755 index 00000000..d147bfce --- /dev/null +++ b/k8s/scope/security/manage_jwt_auth @@ -0,0 +1,118 @@ +#!/bin/bash + +set -euo pipefail + +# This script manages JWT authentication resources for scopes with "exposer" visibility +# It creates/deletes RequestAuthentication and AuthorizationPolicy resources + +ACTION=${ACTION:-"create"} +GATEWAY_NAMESPACE=${GATEWAY_NAMESPACE:-"gateways"} +JWT_ISSUER=${JWT_ISSUER:-"https://api.nullplatform.com/scope"} +JWT_JWKS_URI=${JWT_JWKS_URI:-"https://api.nullplatform.com/scope/.well-known/jwks.json"} + +echo "Managing JWT authentication for scope: $SCOPE_ID" +echo "Action: $ACTION" +echo "Scope visibility: $SCOPE_VISIBILITY" +echo "Scope domain: $SCOPE_DOMAIN" +echo "Gateway name: $GATEWAY_NAME" +echo "Gateway namespace: $GATEWAY_NAMESPACE" + +# Only proceed if scope visibility is "exposer" +if [ "$SCOPE_VISIBILITY" != "exposer" ]; then + echo "Scope visibility is not 'exposer', skipping JWT authentication setup" + return 0 +fi + +# Ensure templates directory exists +TEMPLATE_DIR="$SERVICE_PATH/scope/security/templates" +if [ ! -d "$TEMPLATE_DIR" ]; then + echo "ERROR: Template directory not found: $TEMPLATE_DIR" + exit 1 +fi + +# Create or verify RequestAuthentication (one per cluster) +REQUEST_AUTH_NAME="nullplatform-scope-jwt-auth" +echo "Checking RequestAuthentication: $REQUEST_AUTH_NAME" + +if kubectl get requestauthentication "$REQUEST_AUTH_NAME" -n "$GATEWAY_NAMESPACE" &> /dev/null; then + echo "RequestAuthentication '$REQUEST_AUTH_NAME' already exists, skipping creation" +else + if [ "$ACTION" = "create" ] || [ "$ACTION" = "apply" ]; then + echo "Creating RequestAuthentication: $REQUEST_AUTH_NAME" + + # Build RequestAuthentication template + CONTEXT_PATH="$OUTPUT_DIR/jwt-context-$SCOPE_ID.json" + REQUEST_AUTH_PATH="$OUTPUT_DIR/request-authentication-$SCOPE_ID.yaml" + + echo "$CONTEXT" | jq \ + --arg gateway_namespace "$GATEWAY_NAMESPACE" \ + --arg request_auth_name "$REQUEST_AUTH_NAME" \ + --arg jwt_issuer "$JWT_ISSUER" \ + --arg jwt_jwks_uri "$JWT_JWKS_URI" \ + '. + { + gateway_namespace: $gateway_namespace, + request_auth_name: $request_auth_name, + jwt_issuer: $jwt_issuer, + jwt_jwks_uri: $jwt_jwks_uri + }' > "$CONTEXT_PATH" + + gomplate -c .="$CONTEXT_PATH" \ + --file "$TEMPLATE_DIR/request-authentication.yaml.tmpl" \ + --out "$REQUEST_AUTH_PATH" + + if [ $? -ne 0 ]; then + echo "ERROR: Failed to build RequestAuthentication template" + exit 1 + fi + + kubectl apply -f "$REQUEST_AUTH_PATH" + echo "RequestAuthentication created successfully" + fi +fi + +# Manage AuthorizationPolicy per scope domain +AUTHZ_POLICY_NAME="require-jwt-$(echo "$SCOPE_ID" | sed 's/_/-/g')" +echo "Managing AuthorizationPolicy: $AUTHZ_POLICY_NAME" + +if [ "$ACTION" = "delete" ]; then + echo "Deleting AuthorizationPolicy: $AUTHZ_POLICY_NAME" + + if kubectl get authorizationpolicy "$AUTHZ_POLICY_NAME" -n "$GATEWAY_NAMESPACE" &> /dev/null; then + kubectl delete authorizationpolicy "$AUTHZ_POLICY_NAME" -n "$GATEWAY_NAMESPACE" + echo "AuthorizationPolicy deleted successfully" + else + echo "AuthorizationPolicy '$AUTHZ_POLICY_NAME' does not exist, skipping deletion" + fi +else + # Create or update AuthorizationPolicy + echo "Creating/updating AuthorizationPolicy: $AUTHZ_POLICY_NAME" + + CONTEXT_PATH="$OUTPUT_DIR/jwt-context-$SCOPE_ID.json" + AUTHZ_POLICY_PATH="$OUTPUT_DIR/authorization-policy-$SCOPE_ID.yaml" + + echo "$CONTEXT" | jq \ + --arg gateway_namespace "$GATEWAY_NAMESPACE" \ + --arg authz_policy_name "$AUTHZ_POLICY_NAME" \ + --arg scope_domain "$SCOPE_DOMAIN" \ + --arg jwt_issuer "$JWT_ISSUER" \ + '. + { + gateway_namespace: $gateway_namespace, + authz_policy_name: $authz_policy_name, + scope_domain: $scope_domain, + jwt_issuer: $jwt_issuer + }' > "$CONTEXT_PATH" + + gomplate -c .="$CONTEXT_PATH" \ + --file "$TEMPLATE_DIR/authorization-policy.yaml.tmpl" \ + --out "$AUTHZ_POLICY_PATH" + + if [ $? -ne 0 ]; then + echo "ERROR: Failed to build AuthorizationPolicy template" + exit 1 + fi + + kubectl apply -f "$AUTHZ_POLICY_PATH" + echo "AuthorizationPolicy created/updated successfully" +fi + +echo "JWT authentication management completed" diff --git a/k8s/scope/security/templates/authorization-policy.yaml.tmpl b/k8s/scope/security/templates/authorization-policy.yaml.tmpl new file mode 100644 index 00000000..118152e1 --- /dev/null +++ b/k8s/scope/security/templates/authorization-policy.yaml.tmpl @@ -0,0 +1,28 @@ +--- +apiVersion: security.istio.io/v1 +kind: AuthorizationPolicy +metadata: + name: {{ .authz_policy_name }} + namespace: {{ .gateway_namespace }} + labels: + nullplatform.com/managed-by: endpoint-exposer + nullplatform.com/scope-id: "{{ .scope.id }}" +spec: + action: DENY + selector: + matchLabels: + gateway.networking.k8s.io/gateway-name: {{ .gateway_name }} + rules: + # Deny requests to this scope's domain without valid JWT + - to: + - operation: + hosts: + - "{{ .scope_domain }}" + ports: ["443"] + notPaths: + - /health + - /ready + - /metrics + when: + - key: request.auth.claims[aud] + notValues: ["{{ .scope_domain }}"] \ No newline at end of file diff --git a/k8s/scope/security/templates/request-authentication.yaml.tmpl b/k8s/scope/security/templates/request-authentication.yaml.tmpl new file mode 100644 index 00000000..06a39405 --- /dev/null +++ b/k8s/scope/security/templates/request-authentication.yaml.tmpl @@ -0,0 +1,27 @@ +--- +apiVersion: security.istio.io/v1 +kind: RequestAuthentication +metadata: + name: {{ .request_auth_name }} + namespace: {{ .gateway_namespace }} + labels: + nullplatform.com/managed-by: endpoint-exposer +spec: + selector: + matchLabels: + gateway.networking.k8s.io/gateway-name: {{ .gateway_name }} + jwtRules: + - issuer: "{{ .jwt_issuer }}" + jwksUri: "{{ .jwt_jwks_uri }}" + fromHeaders: + - name: Authorization + prefix: "Bearer " + fromCookies: + - "np_scope_token" + outputClaimToHeaders: + - header: "X-User-ID" + claim: "sub" + - header: "X-User-Email" + claim: "email" + - header: "X-User-Scopes" + claim: "scope" diff --git a/k8s/scope/workflows/create.yaml b/k8s/scope/workflows/create.yaml index adb336c5..18c3e61d 100644 --- a/k8s/scope/workflows/create.yaml +++ b/k8s/scope/workflows/create.yaml @@ -70,3 +70,11 @@ steps: - name: wait on balancer type: script file: "$SERVICE_PATH/scope/wait_on_balancer" + - name: security + type: workflow + steps: + - name: setup jwt authentication + type: script + file: "$SERVICE_PATH/scope/security/manage_jwt_auth" + configuration: + ACTION: create diff --git a/k8s/scope/workflows/delete.yaml b/k8s/scope/workflows/delete.yaml index 541f53ad..4f7c25ba 100644 --- a/k8s/scope/workflows/delete.yaml +++ b/k8s/scope/workflows/delete.yaml @@ -13,6 +13,14 @@ steps: type: environment - name: OUTPUT_DIR type: environment + - name: security + type: workflow + steps: + - name: delete jwt authentication + type: script + file: "$SERVICE_PATH/scope/security/manage_jwt_auth" + configuration: + ACTION: delete - name: networking type: workflow steps: diff --git a/k8s/scope/workflows/update.yaml b/k8s/scope/workflows/update.yaml index b1e48442..b156f93b 100644 --- a/k8s/scope/workflows/update.yaml +++ b/k8s/scope/workflows/update.yaml @@ -5,4 +5,12 @@ steps: type: workflow steps: - name: generate domain - action: skip \ No newline at end of file + action: skip + - name: security + type: workflow + steps: + - name: setup jwt authentication + type: script + file: "$SERVICE_PATH/scope/security/manage_jwt_auth" + configuration: + ACTION: apply \ No newline at end of file From 6c965ab26c47165fc37ff2f92a86a076c18cbeaa Mon Sep 17 00:00:00 2001 From: Javi Date: Wed, 14 Jan 2026 14:49:13 -0300 Subject: [PATCH 2/9] feat: add scope jwt token validation --- k8s/scope/build_context | 16 +++++++--------- k8s/scope/security/manage_jwt_auth | 15 ++++++++------- 2 files changed, 15 insertions(+), 16 deletions(-) diff --git a/k8s/scope/build_context b/k8s/scope/build_context index 1aac6ad0..29f82ce6 100755 --- a/k8s/scope/build_context +++ b/k8s/scope/build_context @@ -36,15 +36,16 @@ USE_ACCOUNT_SLUG=$(echo "$CONTEXT" | jq -r --arg default "$USE_ACCOUNT_SLUG" ' REGION=$(echo "$CONTEXT" | jq -r '.providers["cloud-providers"].account.region // "us-east-1"') SCOPE_VISIBILITY=$(echo "$CONTEXT" | jq -r '.scope.capabilities.visibility') +JWT_AUTHORIZATION_ENABLED=$(echo "$CONTEXT" | jq -r '.scope.capabilities.jwt_validation // false') -if [ "$SCOPE_VISIBILITY" = "internal" ]; then - DOMAIN=$(echo "$CONTEXT" | jq -r --arg private_default "$PRIVATE_DOMAIN" --arg default "$DOMAIN" ' - (.providers["cloud-providers"].networking.private_domain_name // $private_default | if . == "" then empty else . end) // .providers["cloud-providers"].networking.domain_name // $default - ') -else +if [ "$SCOPE_VISIBILITY" = "public" ]; then DOMAIN=$(echo "$CONTEXT" | jq -r --arg default "$DOMAIN" ' .providers["cloud-providers"].networking.domain_name // $default ') +else + DOMAIN=$(echo "$CONTEXT" | jq -r --arg private_default "$PRIVATE_DOMAIN" --arg default "$DOMAIN" ' + (.providers["cloud-providers"].networking.private_domain_name // $private_default | if . == "" then empty else . end) // .providers["cloud-providers"].networking.domain_name // $default + ') fi SCOPE_DOMAIN=$(echo "$CONTEXT" | jq .scope.domain -r) @@ -58,6 +59,7 @@ export APPLICATION_ID=\(capture("application=(?\\d+)").id) SCOPE_ID=$(echo "$CONTEXT" | jq -r '.scope.id') export SCOPE_VISIBILITY +export JWT_AUTHORIZATION_ENABLED export SCOPE_DOMAIN if [ "$SCOPE_VISIBILITY" = "public" ]; then @@ -68,10 +70,6 @@ else export INGRESS_VISIBILITY="internal" GATEWAY_DEFAULT="${PRIVATE_GATEWAY_NAME:-gateway-internal}" export GATEWAY_NAME=$(echo "$CONTEXT" | jq -r --arg default "$GATEWAY_DEFAULT" '.providers["container-orchestration"].gateway.private_name // $default') -else - export INGRESS_VISIBILITY="internet-facing" - GATEWAY_DEFAULT="${PUBLIC_GATEWAY_NAME:-gateway-public}" - export GATEWAY_NAME=$(echo "$CONTEXT" | jq -r --arg default "$GATEWAY_DEFAULT" '.providers["container-orchestration"].gateway.public_name // $default') fi K8S_MODIFIERS="${K8S_MODIFIERS:-"{}"}" diff --git a/k8s/scope/security/manage_jwt_auth b/k8s/scope/security/manage_jwt_auth index d147bfce..87d02691 100755 --- a/k8s/scope/security/manage_jwt_auth +++ b/k8s/scope/security/manage_jwt_auth @@ -10,19 +10,20 @@ GATEWAY_NAMESPACE=${GATEWAY_NAMESPACE:-"gateways"} JWT_ISSUER=${JWT_ISSUER:-"https://api.nullplatform.com/scope"} JWT_JWKS_URI=${JWT_JWKS_URI:-"https://api.nullplatform.com/scope/.well-known/jwks.json"} +echo "JWT authorization enabled: $JWT_AUTHORIZATION_ENABLED" + +# Only proceed if JWT authorization is enabled +if [ "$JWT_AUTHORIZATION_ENABLED" != "true" ]; then + echo "JWT authorization is not enabled for this scope, skipping JWT authentication setup" + exit 0 +fi + echo "Managing JWT authentication for scope: $SCOPE_ID" echo "Action: $ACTION" -echo "Scope visibility: $SCOPE_VISIBILITY" echo "Scope domain: $SCOPE_DOMAIN" echo "Gateway name: $GATEWAY_NAME" echo "Gateway namespace: $GATEWAY_NAMESPACE" -# Only proceed if scope visibility is "exposer" -if [ "$SCOPE_VISIBILITY" != "exposer" ]; then - echo "Scope visibility is not 'exposer', skipping JWT authentication setup" - return 0 -fi - # Ensure templates directory exists TEMPLATE_DIR="$SERVICE_PATH/scope/security/templates" if [ ! -d "$TEMPLATE_DIR" ]; then From f2c7e53a43df08b70b763be10a62098360cfb9b7 Mon Sep 17 00:00:00 2001 From: Javi Date: Wed, 14 Jan 2026 15:11:50 -0300 Subject: [PATCH 3/9] feat: change scope prop ref --- k8s/scope/build_context | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/k8s/scope/build_context b/k8s/scope/build_context index 29f82ce6..720e8b79 100755 --- a/k8s/scope/build_context +++ b/k8s/scope/build_context @@ -36,7 +36,7 @@ USE_ACCOUNT_SLUG=$(echo "$CONTEXT" | jq -r --arg default "$USE_ACCOUNT_SLUG" ' REGION=$(echo "$CONTEXT" | jq -r '.providers["cloud-providers"].account.region // "us-east-1"') SCOPE_VISIBILITY=$(echo "$CONTEXT" | jq -r '.scope.capabilities.visibility') -JWT_AUTHORIZATION_ENABLED=$(echo "$CONTEXT" | jq -r '.scope.capabilities.jwt_validation // false') +JWT_AUTHORIZATION_ENABLED=$(echo "$CONTEXT" | jq -r '.scope.capabilities.jwt_authorization_enabled // false') if [ "$SCOPE_VISIBILITY" = "public" ]; then DOMAIN=$(echo "$CONTEXT" | jq -r --arg default "$DOMAIN" ' From 5083f4a115d0f35a7f1ceeb9630c9dcb3bcb0da0 Mon Sep 17 00:00:00 2001 From: Javi Date: Thu, 15 Jan 2026 14:19:21 -0300 Subject: [PATCH 4/9] fix: continue flow when jwt is deactivated --- k8s/scope/security/manage_jwt_auth | 160 ++++++++++++++--------------- 1 file changed, 79 insertions(+), 81 deletions(-) diff --git a/k8s/scope/security/manage_jwt_auth b/k8s/scope/security/manage_jwt_auth index 87d02691..feb8e4c9 100755 --- a/k8s/scope/security/manage_jwt_auth +++ b/k8s/scope/security/manage_jwt_auth @@ -12,108 +12,106 @@ JWT_JWKS_URI=${JWT_JWKS_URI:-"https://api.nullplatform.com/scope/.well-known/jwk echo "JWT authorization enabled: $JWT_AUTHORIZATION_ENABLED" -# Only proceed if JWT authorization is enabled -if [ "$JWT_AUTHORIZATION_ENABLED" != "true" ]; then - echo "JWT authorization is not enabled for this scope, skipping JWT authentication setup" - exit 0 -fi +if [ "$JWT_AUTHORIZATION_ENABLED" = "true" ]; then + echo "Managing JWT authentication for scope: $SCOPE_ID" + echo "Action: $ACTION" + echo "Scope domain: $SCOPE_DOMAIN" + echo "Gateway name: $GATEWAY_NAME" + echo "Gateway namespace: $GATEWAY_NAMESPACE" + + # Ensure templates directory exists + TEMPLATE_DIR="$SERVICE_PATH/scope/security/templates" + if [ ! -d "$TEMPLATE_DIR" ]; then + echo "ERROR: Template directory not found: $TEMPLATE_DIR" + exit 1 + fi -echo "Managing JWT authentication for scope: $SCOPE_ID" -echo "Action: $ACTION" -echo "Scope domain: $SCOPE_DOMAIN" -echo "Gateway name: $GATEWAY_NAME" -echo "Gateway namespace: $GATEWAY_NAMESPACE" - -# Ensure templates directory exists -TEMPLATE_DIR="$SERVICE_PATH/scope/security/templates" -if [ ! -d "$TEMPLATE_DIR" ]; then - echo "ERROR: Template directory not found: $TEMPLATE_DIR" - exit 1 -fi + # Create or verify RequestAuthentication (one per cluster) + REQUEST_AUTH_NAME="nullplatform-scope-jwt-auth" + echo "Checking RequestAuthentication: $REQUEST_AUTH_NAME" + + if kubectl get requestauthentication "$REQUEST_AUTH_NAME" -n "$GATEWAY_NAMESPACE" &> /dev/null; then + echo "RequestAuthentication '$REQUEST_AUTH_NAME' already exists, skipping creation" + else + if [ "$ACTION" = "create" ] || [ "$ACTION" = "apply" ]; then + echo "Creating RequestAuthentication: $REQUEST_AUTH_NAME" + + # Build RequestAuthentication template + CONTEXT_PATH="$OUTPUT_DIR/jwt-context-$SCOPE_ID.json" + REQUEST_AUTH_PATH="$OUTPUT_DIR/request-authentication-$SCOPE_ID.yaml" + + echo "$CONTEXT" | jq \ + --arg gateway_namespace "$GATEWAY_NAMESPACE" \ + --arg request_auth_name "$REQUEST_AUTH_NAME" \ + --arg jwt_issuer "$JWT_ISSUER" \ + --arg jwt_jwks_uri "$JWT_JWKS_URI" \ + '. + { + gateway_namespace: $gateway_namespace, + request_auth_name: $request_auth_name, + jwt_issuer: $jwt_issuer, + jwt_jwks_uri: $jwt_jwks_uri + }' > "$CONTEXT_PATH" + + gomplate -c .="$CONTEXT_PATH" \ + --file "$TEMPLATE_DIR/request-authentication.yaml.tmpl" \ + --out "$REQUEST_AUTH_PATH" + + if [ $? -ne 0 ]; then + echo "ERROR: Failed to build RequestAuthentication template" + exit 1 + fi + + kubectl apply -f "$REQUEST_AUTH_PATH" + echo "RequestAuthentication created successfully" + fi + fi -# Create or verify RequestAuthentication (one per cluster) -REQUEST_AUTH_NAME="nullplatform-scope-jwt-auth" -echo "Checking RequestAuthentication: $REQUEST_AUTH_NAME" + # Manage AuthorizationPolicy per scope domain + AUTHZ_POLICY_NAME="require-jwt-$(echo "$SCOPE_ID" | sed 's/_/-/g')" + echo "Managing AuthorizationPolicy: $AUTHZ_POLICY_NAME" -if kubectl get requestauthentication "$REQUEST_AUTH_NAME" -n "$GATEWAY_NAMESPACE" &> /dev/null; then - echo "RequestAuthentication '$REQUEST_AUTH_NAME' already exists, skipping creation" -else - if [ "$ACTION" = "create" ] || [ "$ACTION" = "apply" ]; then - echo "Creating RequestAuthentication: $REQUEST_AUTH_NAME" + if [ "$ACTION" = "delete" ]; then + echo "Deleting AuthorizationPolicy: $AUTHZ_POLICY_NAME" + + if kubectl get authorizationpolicy "$AUTHZ_POLICY_NAME" -n "$GATEWAY_NAMESPACE" &> /dev/null; then + kubectl delete authorizationpolicy "$AUTHZ_POLICY_NAME" -n "$GATEWAY_NAMESPACE" + echo "AuthorizationPolicy deleted successfully" + else + echo "AuthorizationPolicy '$AUTHZ_POLICY_NAME' does not exist, skipping deletion" + fi + else + # Create or update AuthorizationPolicy + echo "Creating/updating AuthorizationPolicy: $AUTHZ_POLICY_NAME" - # Build RequestAuthentication template CONTEXT_PATH="$OUTPUT_DIR/jwt-context-$SCOPE_ID.json" - REQUEST_AUTH_PATH="$OUTPUT_DIR/request-authentication-$SCOPE_ID.yaml" + AUTHZ_POLICY_PATH="$OUTPUT_DIR/authorization-policy-$SCOPE_ID.yaml" echo "$CONTEXT" | jq \ --arg gateway_namespace "$GATEWAY_NAMESPACE" \ - --arg request_auth_name "$REQUEST_AUTH_NAME" \ + --arg authz_policy_name "$AUTHZ_POLICY_NAME" \ + --arg scope_domain "$SCOPE_DOMAIN" \ --arg jwt_issuer "$JWT_ISSUER" \ - --arg jwt_jwks_uri "$JWT_JWKS_URI" \ '. + { gateway_namespace: $gateway_namespace, - request_auth_name: $request_auth_name, - jwt_issuer: $jwt_issuer, - jwt_jwks_uri: $jwt_jwks_uri + authz_policy_name: $authz_policy_name, + scope_domain: $scope_domain, + jwt_issuer: $jwt_issuer }' > "$CONTEXT_PATH" gomplate -c .="$CONTEXT_PATH" \ - --file "$TEMPLATE_DIR/request-authentication.yaml.tmpl" \ - --out "$REQUEST_AUTH_PATH" + --file "$TEMPLATE_DIR/authorization-policy.yaml.tmpl" \ + --out "$AUTHZ_POLICY_PATH" if [ $? -ne 0 ]; then - echo "ERROR: Failed to build RequestAuthentication template" + echo "ERROR: Failed to build AuthorizationPolicy template" exit 1 fi - kubectl apply -f "$REQUEST_AUTH_PATH" - echo "RequestAuthentication created successfully" - fi -fi - -# Manage AuthorizationPolicy per scope domain -AUTHZ_POLICY_NAME="require-jwt-$(echo "$SCOPE_ID" | sed 's/_/-/g')" -echo "Managing AuthorizationPolicy: $AUTHZ_POLICY_NAME" - -if [ "$ACTION" = "delete" ]; then - echo "Deleting AuthorizationPolicy: $AUTHZ_POLICY_NAME" - - if kubectl get authorizationpolicy "$AUTHZ_POLICY_NAME" -n "$GATEWAY_NAMESPACE" &> /dev/null; then - kubectl delete authorizationpolicy "$AUTHZ_POLICY_NAME" -n "$GATEWAY_NAMESPACE" - echo "AuthorizationPolicy deleted successfully" - else - echo "AuthorizationPolicy '$AUTHZ_POLICY_NAME' does not exist, skipping deletion" + kubectl apply -f "$AUTHZ_POLICY_PATH" + echo "AuthorizationPolicy created/updated successfully" fi else - # Create or update AuthorizationPolicy - echo "Creating/updating AuthorizationPolicy: $AUTHZ_POLICY_NAME" - - CONTEXT_PATH="$OUTPUT_DIR/jwt-context-$SCOPE_ID.json" - AUTHZ_POLICY_PATH="$OUTPUT_DIR/authorization-policy-$SCOPE_ID.yaml" - - echo "$CONTEXT" | jq \ - --arg gateway_namespace "$GATEWAY_NAMESPACE" \ - --arg authz_policy_name "$AUTHZ_POLICY_NAME" \ - --arg scope_domain "$SCOPE_DOMAIN" \ - --arg jwt_issuer "$JWT_ISSUER" \ - '. + { - gateway_namespace: $gateway_namespace, - authz_policy_name: $authz_policy_name, - scope_domain: $scope_domain, - jwt_issuer: $jwt_issuer - }' > "$CONTEXT_PATH" - - gomplate -c .="$CONTEXT_PATH" \ - --file "$TEMPLATE_DIR/authorization-policy.yaml.tmpl" \ - --out "$AUTHZ_POLICY_PATH" - - if [ $? -ne 0 ]; then - echo "ERROR: Failed to build AuthorizationPolicy template" - exit 1 - fi - - kubectl apply -f "$AUTHZ_POLICY_PATH" - echo "AuthorizationPolicy created/updated successfully" + echo "JWT authorization is not enabled for this scope, skipping JWT authentication setup" fi echo "JWT authentication management completed" From d68aa5790a7417fd81e66155a1f2e36a685be3a0 Mon Sep 17 00:00:00 2001 From: Javi Date: Thu, 15 Jan 2026 17:48:58 -0300 Subject: [PATCH 5/9] feat: continue flow wihtout gateway IP --- .../networking/dns/external_dns/manage_route | 60 ++++++++++--------- 1 file changed, 32 insertions(+), 28 deletions(-) diff --git a/k8s/scope/networking/dns/external_dns/manage_route b/k8s/scope/networking/dns/external_dns/manage_route index 7c8cfdf4..20934882 100644 --- a/k8s/scope/networking/dns/external_dns/manage_route +++ b/k8s/scope/networking/dns/external_dns/manage_route @@ -4,7 +4,7 @@ set -euo pipefail if [ "$ACTION" = "CREATE" ]; then echo "Building DNSEndpoint manifest for ExternalDNS..." - + echo "Getting IP for gateway: $GATEWAY_NAME" GATEWAY_IP=$(kubectl get gateway "$GATEWAY_NAME" -n gateways \ @@ -16,37 +16,39 @@ if [ "$ACTION" = "CREATE" ]; then GATEWAY_IP=$(kubectl get service "$GATEWAY_NAME" -n gateways \ -o jsonpath='{.status.loadBalancer.ingress[0].ip}' 2>/dev/null) fi - + if [ -z "$GATEWAY_IP" ]; then echo "Warning: Could not determine gateway IP address yet, DNSEndpoint will be created later" - exit 0 - fi - - echo "Gateway IP: $GATEWAY_IP" - - DNS_ENDPOINT_TEMPLATE="${DNS_ENDPOINT_TEMPLATE:-$SERVICE_PATH/deployment/templates/dns-endpoint.yaml.tpl}" - - if [ -f "$DNS_ENDPOINT_TEMPLATE" ]; then - DNS_ENDPOINT_FILE="$OUTPUT_DIR/dns-endpoint-$SCOPE_ID.yaml" - CONTEXT_PATH="$OUTPUT_DIR/context-$SCOPE_ID-dns.json" - - echo "$CONTEXT" | jq --arg gateway_ip "$GATEWAY_IP" '. + {gateway_ip: $gateway_ip}' > "$CONTEXT_PATH" - - echo "Building DNSEndpoint Template: $DNS_ENDPOINT_TEMPLATE to $DNS_ENDPOINT_FILE" - - gomplate -c .="$CONTEXT_PATH" \ - --file "$DNS_ENDPOINT_TEMPLATE" \ - --out "$DNS_ENDPOINT_FILE" - - echo "DNSEndpoint manifest created at: $DNS_ENDPOINT_FILE" - - rm "$CONTEXT_PATH" - + echo "Skipping DNSEndpoint creation" else - echo "Error: DNSEndpoint template not found at $DNS_ENDPOINT_TEMPLATE" - exit 1 + echo "Gateway IP: $GATEWAY_IP" + + DNS_ENDPOINT_TEMPLATE="${DNS_ENDPOINT_TEMPLATE:-$SERVICE_PATH/deployment/templates/dns-endpoint.yaml.tpl}" + + if [ -f "$DNS_ENDPOINT_TEMPLATE" ]; then + DNS_ENDPOINT_FILE="$OUTPUT_DIR/dns-endpoint-$SCOPE_ID.yaml" + CONTEXT_PATH="$OUTPUT_DIR/context-$SCOPE_ID-dns.json" + + echo "$CONTEXT" | jq --arg gateway_ip "$GATEWAY_IP" '. + {gateway_ip: $gateway_ip}' > "$CONTEXT_PATH" + + echo "Building DNSEndpoint Template: $DNS_ENDPOINT_TEMPLATE to $DNS_ENDPOINT_FILE" + + gomplate -c .="$CONTEXT_PATH" \ + --file "$DNS_ENDPOINT_TEMPLATE" \ + --out "$DNS_ENDPOINT_FILE" + + echo "DNSEndpoint manifest created at: $DNS_ENDPOINT_FILE" + + rm "$CONTEXT_PATH" + + else + echo "Error: DNSEndpoint template not found at $DNS_ENDPOINT_TEMPLATE" + exit 1 + fi fi + echo "DNSEndpoint management completed" + elif [ "$ACTION" = "DELETE" ]; then echo "Deleting DNSEndpoint for external_dns..." @@ -56,4 +58,6 @@ elif [ "$ACTION" = "DELETE" ]; then kubectl delete dnsendpoint "$DNS_ENDPOINT_NAME" -n "$K8S_NAMESPACE" || echo "DNSEndpoint may already be deleted" echo "DNSEndpoint deletion completed" -fi \ No newline at end of file +fi + +echo "External DNS route management completed" \ No newline at end of file From b67cbe83f6b20233cc1553c8c3df6ac03772b5a0 Mon Sep 17 00:00:00 2001 From: Javi Date: Fri, 16 Jan 2026 09:38:02 -0300 Subject: [PATCH 6/9] feat: adapt flow to get ELB hostname from gateway --- .../templates/dns-endpoint.yaml.tpl | 4 +- .../networking/dns/external_dns/manage_route | 40 ++++++++++++++++--- 2 files changed, 36 insertions(+), 8 deletions(-) diff --git a/k8s/deployment/templates/dns-endpoint.yaml.tpl b/k8s/deployment/templates/dns-endpoint.yaml.tpl index e68e1903..b1dd2340 100644 --- a/k8s/deployment/templates/dns-endpoint.yaml.tpl +++ b/k8s/deployment/templates/dns-endpoint.yaml.tpl @@ -17,6 +17,6 @@ spec: endpoints: - dnsName: {{ .scope.domain }} recordTTL: 60 - recordType: A + recordType: {{ .dns_record_type }} targets: - - "{{ .gateway_ip }}" + - "{{ .gateway_target }}" diff --git a/k8s/scope/networking/dns/external_dns/manage_route b/k8s/scope/networking/dns/external_dns/manage_route index 20934882..baae7591 100644 --- a/k8s/scope/networking/dns/external_dns/manage_route +++ b/k8s/scope/networking/dns/external_dns/manage_route @@ -5,23 +5,49 @@ set -euo pipefail if [ "$ACTION" = "CREATE" ]; then echo "Building DNSEndpoint manifest for ExternalDNS..." - echo "Getting IP for gateway: $GATEWAY_NAME" + echo "Getting address for gateway: $GATEWAY_NAME" + # Try to get IP address first (for cloud providers that support it) GATEWAY_IP=$(kubectl get gateway "$GATEWAY_NAME" -n gateways \ -o jsonpath='{.status.addresses[?(@.type=="IPAddress")].value}' 2>/dev/null) + # If no IP from Gateway resource, try Service IP if [ -z "$GATEWAY_IP" ]; then - echo "Warning: Could not get gateway IP for $GATEWAY_NAME" - + echo "No IP in Gateway resource, checking Service..." GATEWAY_IP=$(kubectl get service "$GATEWAY_NAME" -n gateways \ -o jsonpath='{.status.loadBalancer.ingress[0].ip}' 2>/dev/null) fi + # If still no IP, try to get hostname (for EKS/ELB scenarios) + GATEWAY_HOSTNAME="" + RECORD_TYPE="A" + if [ -z "$GATEWAY_IP" ]; then - echo "Warning: Could not determine gateway IP address yet, DNSEndpoint will be created later" - echo "Skipping DNSEndpoint creation" + echo "No IP found, checking for hostname..." + GATEWAY_HOSTNAME=$(kubectl get gateway "$GATEWAY_NAME" -n gateways \ + -o jsonpath='{.status.addresses[?(@.type=="Hostname")].value}' 2>/dev/null) + + # If no hostname from Gateway resource, try Service hostname + if [ -z "$GATEWAY_HOSTNAME" ]; then + GATEWAY_HOSTNAME=$(kubectl get service "$GATEWAY_NAME" -n gateways \ + -o jsonpath='{.status.loadBalancer.ingress[0].hostname}' 2>/dev/null) + fi + + if [ -n "$GATEWAY_HOSTNAME" ]; then + RECORD_TYPE="CNAME" + echo "Gateway hostname: $GATEWAY_HOSTNAME" + fi else echo "Gateway IP: $GATEWAY_IP" + fi + + # Check if we have either IP or hostname + if [ -z "$GATEWAY_IP" ] && [ -z "$GATEWAY_HOSTNAME" ]; then + echo "Warning: Could not determine gateway address yet, DNSEndpoint will be created later" + echo "Skipping DNSEndpoint creation" + else + # Determine the target value (IP or hostname) + GATEWAY_TARGET="${GATEWAY_IP:-$GATEWAY_HOSTNAME}" DNS_ENDPOINT_TEMPLATE="${DNS_ENDPOINT_TEMPLATE:-$SERVICE_PATH/deployment/templates/dns-endpoint.yaml.tpl}" @@ -29,7 +55,9 @@ if [ "$ACTION" = "CREATE" ]; then DNS_ENDPOINT_FILE="$OUTPUT_DIR/dns-endpoint-$SCOPE_ID.yaml" CONTEXT_PATH="$OUTPUT_DIR/context-$SCOPE_ID-dns.json" - echo "$CONTEXT" | jq --arg gateway_ip "$GATEWAY_IP" '. + {gateway_ip: $gateway_ip}' > "$CONTEXT_PATH" + echo "$CONTEXT" | jq --arg gateway_target "$GATEWAY_TARGET" \ + --arg record_type "$RECORD_TYPE" \ + '. + {gateway_target: $gateway_target, dns_record_type: $record_type}' > "$CONTEXT_PATH" echo "Building DNSEndpoint Template: $DNS_ENDPOINT_TEMPLATE to $DNS_ENDPOINT_FILE" From 0acbf454ed9e080a8c404145c025783e81e137c0 Mon Sep 17 00:00:00 2001 From: Javi Date: Fri, 16 Jan 2026 12:15:58 -0300 Subject: [PATCH 7/9] feat: add doc and name policy updated --- k8s/scope/security/README.md | 312 +++++++++++++++++++++-------- k8s/scope/security/manage_jwt_auth | 2 +- 2 files changed, 227 insertions(+), 87 deletions(-) diff --git a/k8s/scope/security/README.md b/k8s/scope/security/README.md index 62e95b7b..b8dacb21 100644 --- a/k8s/scope/security/README.md +++ b/k8s/scope/security/README.md @@ -1,128 +1,268 @@ # JWT Authentication for Scopes -This directory contains scripts and templates for managing JWT authentication on scopes with `exposer` visibility. +This directory manages JWT-based authentication for scopes with "exposer" visibility using Istio security resources. ## Overview -When a scope is created with `visibility: exposer`, the workflow automatically provisions JWT authentication resources in the Istio gateway namespace: +The JWT authentication flow uses two Istio Custom Resource Definitions (CRDs): + +1. **RequestAuthentication**: Validates JWT tokens from incoming requests +2. **AuthorizationPolicy**: Enforces access control based on JWT claims + +## Important: Istio Gateway Dependency + +**This authentication mechanism ONLY works with Istio gateways.** The resources use Istio-specific CRDs (`security.istio.io/v1`) that are processed by Istio's data plane (Envoy proxy). If your gateway is not Istio-based (e.g., Nginx, Kong, Traefik), you will need to implement authentication using that gateway's native mechanisms. + +## Resources Provisioned + +### 1. RequestAuthentication + +Created once per cluster in the gateway namespace. This resource configures JWT validation. + +**File**: [templates/request-authentication.yaml.tmpl](templates/request-authentication.yaml.tmpl) + +**Example**: +```yaml +apiVersion: security.istio.io/v1 +kind: RequestAuthentication +metadata: + name: nullplatform-scope-jwt-auth + namespace: gateways +spec: + selector: + matchLabels: + gateway.networking.k8s.io/gateway-name: my-gateway + jwtRules: + - issuer: "https://api.nullplatform.com/scope" + jwksUri: "https://api.nullplatform.com/scope/.well-known/jwks.json" + fromHeaders: + - name: Authorization + prefix: "Bearer " + fromCookies: + - "np_scope_token" + outputClaimToHeaders: + - header: "X-User-ID" + claim: "sub" + - header: "X-User-Email" + claim: "email" +``` -1. **RequestAuthentication** - Validates JWT tokens from nullplatform scope API (one per cluster) -2. **AuthorizationPolicy** - Enforces JWT requirement per scope domain +**Key Features**: +- Validates JWTs from `Authorization: Bearer ` header or `np_scope_token` cookie +- Extracts claims and forwards them as headers to backend services +- Non-blocking: Invalid tokens are marked but not rejected at this stage + +### 2. AuthorizationPolicy + +Created per scope domain. This resource enforces JWT requirements. + +**File**: [templates/authorization-policy.yaml.tmpl](templates/authorization-policy.yaml.tmpl) + +**Example**: +```yaml +apiVersion: security.istio.io/v1 +kind: AuthorizationPolicy +metadata: + name: require-jwt-scope-123 + namespace: gateways +spec: + action: DENY + selector: + matchLabels: + gateway.networking.k8s.io/gateway-name: my-gateway + rules: + - to: + - operation: + hosts: + - "my-app.example.com" + ports: ["443"] + notPaths: + - /health + - /ready + - /metrics + when: + - key: request.auth.claims[aud] + notValues: ["my-app.example.com"] +``` -## How it Works +**Key Features**: +- DENY action: Blocks requests that don't meet the conditions +- Requires JWT audience (`aud` claim) to match the scope domain +- Exempts health check endpoints (`/health`, `/ready`, `/metrics`) +- Applies only to HTTPS traffic (port 443) -### Create/Update Workflow +## Flow Diagram -1. The `manage_jwt_auth` script checks if `SCOPE_VISIBILITY` is "exposer" -2. If yes: - - Checks if `RequestAuthentication` exists globally (one per cluster) - - Creates it if it doesn't exist - - Creates/updates an `AuthorizationPolicy` specific to the scope's domain +``` +┌─────────────┐ +│ Client │ +└──────┬──────┘ + │ Request with JWT + │ (Header or Cookie) + ▼ +┌─────────────────────┐ +│ Istio Gateway │ +│ (Envoy Proxy) │ +└──────┬──────────────┘ + │ + ▼ +┌──────────────────────────────┐ +│ RequestAuthentication │ +│ - Validates JWT signature │ +│ - Checks issuer/expiration │ +│ - Extracts claims │ +└──────┬───────────────────────┘ + │ + ▼ +┌──────────────────────────────┐ +│ AuthorizationPolicy │ +│ - Checks aud claim matches │ +│ - DENY if invalid/missing │ +└──────┬───────────────────────┘ + │ + ▼ (if authorized) +┌─────────────────────┐ +│ Backend Service │ +│ + Headers: │ +│ X-User-ID │ +│ X-User-Email │ +└─────────────────────┘ +``` -### Delete Workflow +## Script Usage -1. Deletes the scope-specific `AuthorizationPolicy` -2. Leaves the global `RequestAuthentication` intact (shared by all scopes) +The [manage_jwt_auth](manage_jwt_auth) script handles resource lifecycle: -## Configuration +### Environment Variables -The following environment variables are used: +| Variable | Default | Description | +|----------|---------|-------------| +| `JWT_AUTHORIZATION_ENABLED` | - | Set to "true" to enable JWT auth | +| `ACTION` | "create" | Action to perform: "create", "apply", or "delete" | +| `GATEWAY_NAMESPACE` | "gateways" | Namespace where gateway resources exist | +| `JWT_ISSUER` | `https://api.nullplatform.com/scope` | JWT issuer URL | +| `JWT_JWKS_URI` | `https://api.nullplatform.com/scope/.well-known/jwks.json` | JWKS endpoint for key verification | +| `SCOPE_ID` | - | Unique scope identifier | +| `SCOPE_DOMAIN` | - | Domain for the scope (e.g., `app.example.com`) | +| `GATEWAY_NAME` | - | Name of the Istio gateway | -- `SCOPE_VISIBILITY` - Must be "exposer" to enable JWT auth -- `SCOPE_DOMAIN` - The domain to protect (e.g., `app.example.com`) -- `GATEWAY_NAME` - The Istio gateway name (e.g., `gateway-public`) -- `GATEWAY_NAMESPACE` - Namespace where gateway resources live (default: `gateways`) -- `JWT_ISSUER` - JWT issuer URL (default: `https://api.nullplatform.com/scope`) -- `JWT_JWKS_URI` - JWKS endpoint (default: `https://api.nullplatform.com/scope/.well-known/jwks.json`) +### Create Resources -## Resources Created +```bash +export JWT_AUTHORIZATION_ENABLED=true +export SCOPE_ID=my_scope_123 +export SCOPE_DOMAIN=my-app.example.com +export GATEWAY_NAME=my-gateway +export ACTION=create -### RequestAuthentication: `nullplatform-scope-jwt-auth` +./manage_jwt_auth +``` -- **Location**: `gateways` namespace -- **Scope**: Cluster-wide (one instance) -- **Function**: Validates JWT signature and extracts claims -- **Behavior**: Does not enforce authentication (that's done by AuthorizationPolicy) +### Delete Authorization Policy -### AuthorizationPolicy: `require-jwt-{scope-id}` +```bash +export JWT_AUTHORIZATION_ENABLED=true +export SCOPE_ID=my_scope_123 +export ACTION=delete -- **Location**: `gateways` namespace -- **Scope**: Per scope domain -- **Function**: Denies access without valid JWT -- **Action**: DENY -- **Applies to**: Specific scope domain only -- **Exceptions**: Health endpoints (`/health`, `/ready`, `/metrics`) +./manage_jwt_auth +``` ## JWT Token Requirements -Tokens must: -- Be signed by `https://api.nullplatform.com/scope` -- Have valid signature (verified against JWKS) -- Include `iss` claim matching the issuer -- Include `aud` claim matching the target domain -- Not be expired +Valid JWT tokens must include: + +- **`iss` (issuer)**: Must match `JWT_ISSUER` (default: `https://api.nullplatform.com/scope`) +- **`aud` (audience)**: Must match the scope domain (e.g., `my-app.example.com`) +- **`sub` (subject)**: User identifier +- **`email`**: User email +- **`exp` (expiration)**: Token must not be expired ## Token Sources -The system accepts JWT tokens from: -1. `Authorization` header with `Bearer ` prefix -2. `np_scope_token` cookie +The RequestAuthentication accepts tokens from: + +1. **Authorization Header**: `Authorization: Bearer ` +2. **Cookie**: `np_scope_token=` + +This allows both API clients (using headers) and browsers (using cookies) to authenticate. -## Extracted Claims +## Exempted Paths -The following JWT claims are extracted to HTTP headers: -- `sub` → `X-User-ID` -- `email` → `X-User-Email` -- `scope` → `X-User-Scopes` +The following paths are exempted from JWT validation to support infrastructure monitoring: -## Example +- `/health` - Health check endpoint +- `/ready` - Readiness probe endpoint +- `/metrics` - Metrics collection endpoint + +## Troubleshooting -For a scope with: -- ID: `playground-floppy-bird-api-production-kjstb` -- Domain: `playground-floppy-bird-api-production-kjstb.edenred.nullimplementation.com` -- Visibility: `exposer` +### Request Rejected with 403 -The workflow creates: -1. `RequestAuthentication/nullplatform-scope-jwt-auth` (if not exists) -2. `AuthorizationPolicy/require-jwt-playground-floppy-bird-api-production-kjstb` +**Symptom**: Requests to your domain return 403 Forbidden. -Access behavior: -- ✅ Requests with valid JWT → Allowed -- ❌ Requests without JWT → 403 RBAC Denied -- ✅ Health endpoints → Always allowed +**Possible Causes**: +1. Missing or invalid JWT token +2. Token `aud` claim doesn't match the domain +3. Token expired or signature invalid +4. Token issuer doesn't match configuration -## Files +**Debug Steps**: +```bash +# Check RequestAuthentication exists +kubectl get requestauthentication -n gateways +# Check AuthorizationPolicy exists for your scope +kubectl get authorizationpolicy -n gateways | grep require-jwt + +# View AuthorizationPolicy details +kubectl describe authorizationpolicy require-jwt- -n gateways + +# Check Istio proxy logs +kubectl logs -n gateways -c istio-proxy ``` -scope/security/ -├── README.md # This file -├── manage_jwt_auth # Main script -└── templates/ - ├── request-authentication.yaml.tmpl # RequestAuthentication template - └── authorization-policy.yaml.tmpl # AuthorizationPolicy template + +### Token Validation Fails + +**Symptom**: Logs show JWT validation errors. + +**Check**: +```bash +# Verify JWKS URI is accessible +curl https://api.nullplatform.com/scope/.well-known/jwks.json + +# Decode your JWT to check claims (use jwt.io) +# Verify: +# - iss matches JWT_ISSUER +# - aud matches SCOPE_DOMAIN +# - exp is in the future ``` -## Troubleshooting +### RequestAuthentication Not Applied + +**Symptom**: JWT validation doesn't occur. -### JWT validation failing +**Check**: +```bash +# Verify gateway has the correct label +kubectl get gateway -n gateways -o yaml | grep gateway-name + +# Verify RequestAuthentication selector matches +kubectl get requestauthentication nullplatform-scope-jwt-auth -n gateways -o yaml +``` -Check that: -1. Token is not expired -2. Token `aud` claim matches the domain being accessed -3. Token `iss` claim is `https://api.nullplatform.com/scope` -4. JWKS endpoint is accessible from the cluster +## Alternatives for Non-Istio Gateways -### AuthorizationPolicy not working +If you cannot use Istio, consider these alternatives: -Verify: -1. Scope visibility is "exposer" -2. AuthorizationPolicy exists: `kubectl get authorizationpolicy -n gateways` -3. Policy selector matches gateway pods -4. Check Envoy logs: `kubectl logs -n gateways -f` +- **Nginx Ingress Controller**: Use `auth-url` annotation with external auth service +- **Kong Gateway**: Use Kong JWT plugin +- **Traefik**: Use ForwardAuth middleware +- **AWS ALB**: Use AWS Cognito User Pools or Lambda authorizers +- **Application-level**: Implement JWT validation in your application code -### RequestAuthentication not created +## References -Check: -1. Template files exist in `scope/security/templates/` -2. Gomplate is installed and working -3. kubectl has permissions to create resources in gateways namespace +- [Istio RequestAuthentication Documentation](https://istio.io/latest/docs/reference/config/security/request_authentication/) +- [Istio AuthorizationPolicy Documentation](https://istio.io/latest/docs/reference/config/security/authorization-policy/) +- [JWT Best Practices](https://datatracker.ietf.org/doc/html/rfc8725) diff --git a/k8s/scope/security/manage_jwt_auth b/k8s/scope/security/manage_jwt_auth index feb8e4c9..fa5db706 100755 --- a/k8s/scope/security/manage_jwt_auth +++ b/k8s/scope/security/manage_jwt_auth @@ -67,7 +67,7 @@ if [ "$JWT_AUTHORIZATION_ENABLED" = "true" ]; then fi # Manage AuthorizationPolicy per scope domain - AUTHZ_POLICY_NAME="require-jwt-$(echo "$SCOPE_ID" | sed 's/_/-/g')" + AUTHZ_POLICY_NAME="require-nullplatform-jwt-${SCOPE_SLUG}-${SCOPE_ID}" echo "Managing AuthorizationPolicy: $AUTHZ_POLICY_NAME" if [ "$ACTION" = "delete" ]; then From 5190fa9d1c203e8ba10811e44dc468457c1e9028 Mon Sep 17 00:00:00 2001 From: Javi Date: Fri, 16 Jan 2026 12:37:13 -0300 Subject: [PATCH 8/9] feat: healtcheck path --- k8s/scope/security/README.md | 120 +----------------- k8s/scope/security/manage_jwt_auth | 8 +- .../templates/authorization-policy.yaml.tmpl | 4 +- 3 files changed, 10 insertions(+), 122 deletions(-) diff --git a/k8s/scope/security/README.md b/k8s/scope/security/README.md index b8dacb21..a2e2719a 100644 --- a/k8s/scope/security/README.md +++ b/k8s/scope/security/README.md @@ -1,6 +1,6 @@ # JWT Authentication for Scopes -This directory manages JWT-based authentication for scopes with "exposer" visibility using Istio security resources. +This directory manages JWT-based authentication for scopes with "jwt_authorization_enabled" using Istio security resources. ## Overview @@ -78,8 +78,6 @@ spec: ports: ["443"] notPaths: - /health - - /ready - - /metrics when: - key: request.auth.claims[aud] notValues: ["my-app.example.com"] @@ -88,7 +86,7 @@ spec: **Key Features**: - DENY action: Blocks requests that don't meet the conditions - Requires JWT audience (`aud` claim) to match the scope domain -- Exempts health check endpoints (`/health`, `/ready`, `/metrics`) +- Exempts health check endpoints (`/health`) - Applies only to HTTPS traffic (port 443) ## Flow Diagram @@ -146,120 +144,6 @@ The [manage_jwt_auth](manage_jwt_auth) script handles resource lifecycle: | `SCOPE_DOMAIN` | - | Domain for the scope (e.g., `app.example.com`) | | `GATEWAY_NAME` | - | Name of the Istio gateway | -### Create Resources - -```bash -export JWT_AUTHORIZATION_ENABLED=true -export SCOPE_ID=my_scope_123 -export SCOPE_DOMAIN=my-app.example.com -export GATEWAY_NAME=my-gateway -export ACTION=create - -./manage_jwt_auth -``` - -### Delete Authorization Policy - -```bash -export JWT_AUTHORIZATION_ENABLED=true -export SCOPE_ID=my_scope_123 -export ACTION=delete - -./manage_jwt_auth -``` - -## JWT Token Requirements - -Valid JWT tokens must include: - -- **`iss` (issuer)**: Must match `JWT_ISSUER` (default: `https://api.nullplatform.com/scope`) -- **`aud` (audience)**: Must match the scope domain (e.g., `my-app.example.com`) -- **`sub` (subject)**: User identifier -- **`email`**: User email -- **`exp` (expiration)**: Token must not be expired - -## Token Sources - -The RequestAuthentication accepts tokens from: - -1. **Authorization Header**: `Authorization: Bearer ` -2. **Cookie**: `np_scope_token=` - -This allows both API clients (using headers) and browsers (using cookies) to authenticate. - -## Exempted Paths - -The following paths are exempted from JWT validation to support infrastructure monitoring: - -- `/health` - Health check endpoint -- `/ready` - Readiness probe endpoint -- `/metrics` - Metrics collection endpoint - -## Troubleshooting - -### Request Rejected with 403 - -**Symptom**: Requests to your domain return 403 Forbidden. - -**Possible Causes**: -1. Missing or invalid JWT token -2. Token `aud` claim doesn't match the domain -3. Token expired or signature invalid -4. Token issuer doesn't match configuration - -**Debug Steps**: -```bash -# Check RequestAuthentication exists -kubectl get requestauthentication -n gateways - -# Check AuthorizationPolicy exists for your scope -kubectl get authorizationpolicy -n gateways | grep require-jwt - -# View AuthorizationPolicy details -kubectl describe authorizationpolicy require-jwt- -n gateways - -# Check Istio proxy logs -kubectl logs -n gateways -c istio-proxy -``` - -### Token Validation Fails - -**Symptom**: Logs show JWT validation errors. - -**Check**: -```bash -# Verify JWKS URI is accessible -curl https://api.nullplatform.com/scope/.well-known/jwks.json - -# Decode your JWT to check claims (use jwt.io) -# Verify: -# - iss matches JWT_ISSUER -# - aud matches SCOPE_DOMAIN -# - exp is in the future -``` - -### RequestAuthentication Not Applied - -**Symptom**: JWT validation doesn't occur. - -**Check**: -```bash -# Verify gateway has the correct label -kubectl get gateway -n gateways -o yaml | grep gateway-name - -# Verify RequestAuthentication selector matches -kubectl get requestauthentication nullplatform-scope-jwt-auth -n gateways -o yaml -``` - -## Alternatives for Non-Istio Gateways - -If you cannot use Istio, consider these alternatives: - -- **Nginx Ingress Controller**: Use `auth-url` annotation with external auth service -- **Kong Gateway**: Use Kong JWT plugin -- **Traefik**: Use ForwardAuth middleware -- **AWS ALB**: Use AWS Cognito User Pools or Lambda authorizers -- **Application-level**: Implement JWT validation in your application code ## References diff --git a/k8s/scope/security/manage_jwt_auth b/k8s/scope/security/manage_jwt_auth index fa5db706..24326a2b 100755 --- a/k8s/scope/security/manage_jwt_auth +++ b/k8s/scope/security/manage_jwt_auth @@ -83,6 +83,10 @@ if [ "$JWT_AUTHORIZATION_ENABLED" = "true" ]; then # Create or update AuthorizationPolicy echo "Creating/updating AuthorizationPolicy: $AUTHZ_POLICY_NAME" + # Extract health check path from scope context + HEALTH_CHECK_PATH=$(echo "$CONTEXT" | jq -r '.scope.capabilities.health_check.path // "/health"') + echo "Health check path: $HEALTH_CHECK_PATH" + CONTEXT_PATH="$OUTPUT_DIR/jwt-context-$SCOPE_ID.json" AUTHZ_POLICY_PATH="$OUTPUT_DIR/authorization-policy-$SCOPE_ID.yaml" @@ -91,11 +95,13 @@ if [ "$JWT_AUTHORIZATION_ENABLED" = "true" ]; then --arg authz_policy_name "$AUTHZ_POLICY_NAME" \ --arg scope_domain "$SCOPE_DOMAIN" \ --arg jwt_issuer "$JWT_ISSUER" \ + --arg health_check_path "$HEALTH_CHECK_PATH" \ '. + { gateway_namespace: $gateway_namespace, authz_policy_name: $authz_policy_name, scope_domain: $scope_domain, - jwt_issuer: $jwt_issuer + jwt_issuer: $jwt_issuer, + health_check_path: $health_check_path }' > "$CONTEXT_PATH" gomplate -c .="$CONTEXT_PATH" \ diff --git a/k8s/scope/security/templates/authorization-policy.yaml.tmpl b/k8s/scope/security/templates/authorization-policy.yaml.tmpl index 118152e1..9e5f7d7c 100644 --- a/k8s/scope/security/templates/authorization-policy.yaml.tmpl +++ b/k8s/scope/security/templates/authorization-policy.yaml.tmpl @@ -20,9 +20,7 @@ spec: - "{{ .scope_domain }}" ports: ["443"] notPaths: - - /health - - /ready - - /metrics + - "{{ .health_check_path }}" when: - key: request.auth.claims[aud] notValues: ["{{ .scope_domain }}"] \ No newline at end of file From f4723d85bb001e69e241854221b46de9df161717 Mon Sep 17 00:00:00 2001 From: Javi Date: Fri, 16 Jan 2026 12:40:21 -0300 Subject: [PATCH 9/9] feat: healtcheck path --- k8s/scope/security/README.md | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/k8s/scope/security/README.md b/k8s/scope/security/README.md index a2e2719a..7b51b82b 100644 --- a/k8s/scope/security/README.md +++ b/k8s/scope/security/README.md @@ -127,24 +127,6 @@ spec: └─────────────────────┘ ``` -## Script Usage - -The [manage_jwt_auth](manage_jwt_auth) script handles resource lifecycle: - -### Environment Variables - -| Variable | Default | Description | -|----------|---------|-------------| -| `JWT_AUTHORIZATION_ENABLED` | - | Set to "true" to enable JWT auth | -| `ACTION` | "create" | Action to perform: "create", "apply", or "delete" | -| `GATEWAY_NAMESPACE` | "gateways" | Namespace where gateway resources exist | -| `JWT_ISSUER` | `https://api.nullplatform.com/scope` | JWT issuer URL | -| `JWT_JWKS_URI` | `https://api.nullplatform.com/scope/.well-known/jwks.json` | JWKS endpoint for key verification | -| `SCOPE_ID` | - | Unique scope identifier | -| `SCOPE_DOMAIN` | - | Domain for the scope (e.g., `app.example.com`) | -| `GATEWAY_NAME` | - | Name of the Istio gateway | - - ## References - [Istio RequestAuthentication Documentation](https://istio.io/latest/docs/reference/config/security/request_authentication/)