Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions k8s/deployment/templates/dns-endpoint.yaml.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,6 @@ spec:
endpoints:
- dnsName: {{ .scope.domain }}
recordTTL: 60
recordType: A
recordType: {{ .dns_record_type }}
targets:
- "{{ .gateway_ip }}"
- "{{ .gateway_target }}"
2 changes: 2 additions & 0 deletions k8s/scope/build_context
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +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_authorization_enabled // false')

if [ "$SCOPE_VISIBILITY" = "public" ]; then
DOMAIN=$(echo "$CONTEXT" | jq -r --arg default "$DOMAIN" '
Expand All @@ -58,6 +59,7 @@ export APPLICATION_ID=\(capture("application=(?<id>\\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
Expand Down
94 changes: 63 additions & 31 deletions k8s/scope/networking/dns/external_dns/manage_route
Original file line number Diff line number Diff line change
Expand Up @@ -4,49 +4,79 @@ 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"
exit 0
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

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"


# 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
echo "Error: DNSEndpoint template not found at $DNS_ENDPOINT_TEMPLATE"
exit 1
# 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}"

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_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"

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..."

Expand All @@ -56,4 +86,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
fi

echo "External DNS route management completed"
134 changes: 134 additions & 0 deletions k8s/scope/security/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
# JWT Authentication for Scopes

This directory manages JWT-based authentication for scopes with "jwt_authorization_enabled" using Istio security resources.

## Overview

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"
```

**Key Features**:
- Validates JWTs from `Authorization: Bearer <token>` 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
when:
- key: request.auth.claims[aud]
notValues: ["my-app.example.com"]
```

**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`)
- Applies only to HTTPS traffic (port 443)

## Flow Diagram

```
┌─────────────┐
│ 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 │
└─────────────────────┘
```

## References

- [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)
Loading