# Server Jump Gateway Design

## Introduction

This document proposes a `Server Jump Gateway` built on top of the current Jump Authorization, Jump Pod, and Jump Pod Agent flow. The goal is to let internal users and AI agents access Kubernetes API servers through a standard `kubeconfig`, while reusing the existing approval flow, RBAC injection, and Jump Pod lifecycle management.

V1 works as follows:

- Deploy a standalone Gateway Server behind an ALB.
- DevOps generates a Gateway-facing `kubeconfig` and an internal JWT for an approved authorization through `InternalTokenService`.
- The Gateway verifies the JWT offline through DevOps internal JWKS (`/api/v1/devops-internal/itoken/jwks`).
- The Gateway resolves the target `cred_id` from the request path and fetches a single-destination kubeconfig from Jump Pod Agent.
- The Gateway proxies the request to the real Kubernetes API Server.

## Motivation or Background

The repository already supports Kubernetes access authorization and credential preparation inside a Jump Pod, but the current flow is optimized for "enter the Jump Pod, then use `kubectl`":

- `pkg/controller/jump/jump_controller.go` creates and queries Jump Authorizations and returns Jump Pod connection parameters.
- `pkg/service/jump/jumppod/jumper/jumper_k8s.go` creates temporary cluster tokens and writes kubeconfigs into the Jump Pod using `cred_id=<destination_type>.<destination_id>`.
- `pkg/service/jump/jumppod/agent/k8s_cred_store.go` merges per-destination kubeconfigs into one `config` file.
- `cmd/jump-pod-agent/main.go` already exposes `POST /kubernetes/rebuild` and `GET /kubernetes/credential` for kubeconfig read.

That is sufficient for human access from inside the Jump Pod, but not for these scenarios:

- Internal automation tools using a standard `kubeconfig`
- AI agents or programmatic clients using `client-go`, Python, or JS Kubernetes clients
- A single ALB endpoint that centralizes authentication, auditing, and target selection in a stateless service

The new Gateway keeps the existing "prepare credentials in Jump Pod after authorization" flow, but changes the access model from "log in to Jump Pod" to "use a DevOps-issued kubeconfig against Gateway."

### Goals

- Support standard `kubeconfig` access to Kubernetes through Gateway
- Reuse the existing Jump Authorization approval and expiration flow
- Reuse the existing per-destination kubeconfig generation logic
- Let Gateway perform offline verification, target validation, and auditing
- Keep Gateway stateless and horizontally scalable

### Non-goals

- Online token revocation or introspection in V1
- Cross-VPC or cross-cluster access to Jump Pod Agent in V1
- Exposing the aggregated Jump Pod kubeconfig
- Adding a new persistent audit store in V1

## Detailed Design

### 1. High-level Architecture

```text
User / Internal Tool / AI Agent
            |
            v
      DevOps Server
  (authorization + kubeconfig/jwt generation + JWKS)
            |
            v
         kubeconfig
            |
            v
Client -> ALB -> Server Jump Gateway -> Jump Pod Agent -> target API Server
```

Responsibilities:

- DevOps continues to create and drive Jump Authorizations and inject target RBAC and kubeconfig into the Jump Pod.
- DevOps reuses `InternalTokenService.IssueJWTToken` to issue Gateway access JWTs and reuses `InternalTokenController.GetJWKS` to publish the verification key set.
- The client uses the DevOps-issued kubeconfig and encodes the target `cred_id` in the request path.
- Gateway verifies the JWT, validates the target, fetches the single-destination kubeconfig, and proxies the request.
- Jump Pod Agent only serves prepared kubeconfigs by `cred_id`; it does not make authorization decisions.

### 2. Current Implementation Baseline

The design keeps these existing behaviors unchanged:

- Jump Authorization remains the admission entry point for Kubernetes access.
- `jumper.GetCredID(dest)` remains the stable target identifier: `<destination_type>.<destination_id>`.
- `K8sTokenJumper.prepareToken()` writes one primary context per destination and, when `EnableGatewayAccess=true`, an extra Gateway context keyed by `gateway/<destination_type>.<destination_id>`.
- `KubernetesCredStore.Rebuild()` still merges those kubeconfigs into the output `config`.
- `pkg/service/itoken` already provides internal JWT issuing (`IssueJWTToken`) and public key export (`GetJWKS`).
- `pkg/controller/itoken` already exposes `GET /api/v1/devops-internal/itoken/jwks`.
- DI is already wired in `pkg/infra/deps/deps.go` for both `itokensvc.NewService` and `itokenctrl.NewController`.

V1 only adds the missing pieces on top:

1. A DevOps API to generate Gateway kubeconfig and JWT for clients
2. A Jump Pod Agent API to return one kubeconfig by `cred_id`
3. A standalone Gateway Server

### 3. Gateway Server

#### 3.1 Deployment Model

Gateway runs as a new standalone binary, ideally `cmd/jump-gateway`, alongside `cmd/devops-server` and `cmd/jump-pod-agent`.

Gateway does not depend on the database and does not manage approvals or authorization state. It depends only on:

- DevOps JWKS for offline JWT verification
- Network reachability to Jump Pod Agent
- HTTP traffic from ALB

#### 3.2 Request Path

Each cluster in the client `kubeconfig` points to the same ALB domain. The target is encoded in the path:

```yaml
apiVersion: v1
kind: Config

clusters:
- name: dedicated-123
  cluster:
    server: https://jump-gateway.internal/k8s/clusters/Z2F0ZXdheS9kZWRpY2F0ZWQuMTIz
    certificate-authority-data: <ALB_CA_DATA>

users:
- name: gateway-user
  user:
    token: <DEVOPS_SIGNED_JWT>

contexts:
- name: gateway/dedicated.123
  context:
    cluster: dedicated-123
    user: gateway-user

current-context: gateway/dedicated.123
```

Here `Z2F0ZXdheS9kZWRpY2F0ZWQuMTIz` is `base64("gateway/dedicated.123")`. The path encodes `cred_id`, not raw `destination_id`, so Gateway does not need to infer the destination type again.

#### 3.3 Request Handling Flow

For each request, Gateway:

1. Reads `Authorization: Bearer <jwt>`.
2. Verifies signature, `iss`, `aud`, and `exp` through DevOps JWKS.
3. Extracts and decodes `cred_id` from `/k8s/clusters/{base64_cred_id}/...`.
4. Checks that `cred_id` is listed in the JWT `destinations` claim.
5. Reads `jump_pod_namespace` and `jump_pod_name` from the JWT.
6. Calls `GET /kubernetes/credential?cred_id=<cred_id>` on the corresponding Jump Pod Agent.
7. Builds the downstream `transport` and `reverse proxy` from that kubeconfig.
8. Rewrites the path to the real Kubernetes API path and forwards the request.
9. Emits audit logs and basic metrics.
10. Returns the downstream response unchanged.

#### 3.4 Authorization Model

Gateway uses self-contained JWTs and does not query DevOps on each request.

V1 consistency model:

- JWTs use a short TTL.
- Revoked, closed, or expired authorizations do not immediately revoke already issued JWTs.
- A client must fetch a new kubeconfig/JWT after `exp`.

This keeps Gateway simple and fully offline, at the cost of a short revocation window.

#### 3.5 Audit Fields

Each Gateway request should log at least:

- `request_id`
- `user_email`
- `auth_id`
- `privilege`
- `cred_id`
- `destination_type`
- `destination_id`
- `method`
- `path`
- `query`
- `status_code`
- `latency_ms`
- `allow_or_deny`
- `deny_reason`

V1 only requires structured logs and metrics, not a new database table.

### 4. DevOps Changes

DevOps changes are ordered as: authorization contract -> jump preparation behavior -> gateway kubeconfig/JWT issuance -> JWKS/JWT verification contract.

#### 4.1 Authorization Contract and Persistence

Add `enable_gateway_access` (bool, default `false`) to authorization creation and persist it with the authorization record.

Expected additions:

- `api/jump.CreateAuthorizationReq.enable_gateway_access`
- `pkg/service/jump.CreateAuthorizationParam.EnableGatewayAccess`
- `entity/jump.Authorization.EnableGatewayAccess`
- `models/jump.Authorization.enable_gateway_access`
- `jump_authorizations.enable_gateway_access` column (default `false`)

The field is written when creating authorization and is later consumed in the `Creating` phase to decide whether Gateway RBAC resources should be provisioned.

#### 4.2 Jump Preparation When Gateway Access Enabled

During authorization `Creating` phase (when `PrepareJump` runs), if `EnableGatewayAccess=true`, DevOps should provision extra Gateway-only read-only RBAC resources in addition to existing `jump-<id>` resources.

Resource naming:

- Use `jump-gateway-<id>`.
- `<id>` must use the same ID source as `ResourceNamer`/`JumpClusterRole` (same lifecycle key as current `jump-<id>` naming).

Resources:

- `ServiceAccount`: `jump-gateway-<id>`
- `ClusterRole`: `jump-gateway-<id>`
- `ClusterRoleBinding`: `jump-gateway-<id>`

Permission model:

- Fixed to read-only (`get/list/watch`), aligned with existing `read_only` semantics.
- Does not escalate with authorization privilege (`read_write`/`admin`).

Lifecycle:

- Creation path is idempotent (`create-or-update`).
- Cancellation/expiration/recycle paths clean up `jump-gateway-<id>` together with the primary Jump resources.
- Credential lifecycle is dual-path for Kubernetes jumps:
  - Primary credential ID: `<destination_type>.<destination_id>`
  - Gateway credential ID: `gateway/<destination_type>.<destination_id>` (only when `EnableGatewayAccess=true`)
  - Refresh updates both credentials when the jump attribute `k8s.enable_gateway_access=true`.
  - Finalize always removes both primary and gateway credentials.

#### 4.3 Generate Gateway kubeconfig and JWT

Extend `GetConnection` with `method=gateway` for an authorization that is created, still valid, and already has accessible jump targets prepared. It returns:

- Gateway kubeconfig content
- Internal JWT (`connection_token`)
- `server_urls` field in response is currently reserved and returned as an empty map

Current response shape:

```json
{
  "connection": {
    "gateway": {
      "server_urls": {},
      "connection_token": "<jwt>",
      "kubeconfig": "<yaml>"
    }
  }
}
```

Note: in current implementation `server_urls` is an empty object (`{}`), while per-destination Gateway URLs are encoded in `kubeconfig` cluster server addresses.

Input remains `authorization_id`, and caller must be the authorization `applicant`. For this method, `authorization.phase` must be `created`, `expire_at` must exist, and `expire_at` must be in the future.

When generating kubeconfig:

- Iterate Kubernetes destinations from authorization
- Compute `cred_id=gateway/<destination_type>.<destination_id>` for each destination
- Set `cluster.server` to `https://<gateway-host>/k8s/clusters/{base64(cred_id)}`
- Reuse one JWT across all contexts

When issuing JWT:

- `Subject` is applicant email
- `Audience` is `services.jump.gateway.audience`
- `ExpiresAt` is `authorization.expire_at`
- `CustomClaims` include `user_email`, `auth_id`, `privilege`, `destinations`, `jump_pod_namespace`, and `jump_pod_name`

This keeps existing Jump Pod kubeconfig generation unchanged and adds a separate client-facing Gateway kubeconfig.

#### 4.4 Reuse Existing Internal JWKS and JWT Claims

Gateway reuses existing internal JWKS endpoint:

- `GET /api/v1/devops-internal/itoken/jwks`
- `InternalTokenController.GetJWKS`
- `InternalTokenService.GetJWKS`

JWT/JWKS model remains unchanged:

- DevOps signs JWTs via `InternalTokenService` private key (`RS256`)
- Gateway verifies by JWKS/public key only
- No new token-signing subsystem is introduced

The JWT should include at least:

- `iss`
- `aud`
- `sub`
- `user_email`
- `auth_id`
- `privilege`
- `destinations`
- `jump_pod_namespace`
- `jump_pod_name`
- `iat`
- `exp`
- `jti`

Claim and token-shape constraints from current `InternalTokenService` implementation:

- `iss` is populated from `consts.ServerAddr()` (derived from runtime env/partition), not a free-form request field.
- `aud` is required and must be explicitly passed by the caller when issuing a token.
- Signing algorithm is `RS256`, and JWT header includes `kid` from `services.internalToken.keyID`.
- Expiration uses `services.internalToken.defaultTTL` when `ExpiresAt` is absent, and uses `ExpiresAt` when provided.
- Custom claims cannot overwrite reserved claims: `iss`, `aud`, `sub`, `iat`, `exp`, `jti`.

Current `destinations` shape:

```json
[
  "123",
  "infra-apiserver"
]
```

That is, current implementation puts destination IDs (string array) into `destinations`, not a structured object list.

The JWT carries only access-control data, not kubeconfig content.

### 5. Jump Pod Agent Changes

#### 5.1 Add a Read-only kubeconfig API

Current Jump Pod Agent APIs:

- `POST /kubernetes/rebuild`
- `GET /kubernetes/credential`
- `POST /tailscale/up`
- `POST /tailscale/down`
- `GET /tailscale/status`

Support query:

- repeated `cred_id` query parameters, e.g. `GET /kubernetes/credential?cred_id=gateway/dedicated.123&cred_id=dedicated.123`

Recommended response:

```json
{
  "kubeconfig": "<yaml>" // after merge
}
```

#### 5.2 Data Source

Agent currently builds the returned kubeconfig by walking input credential files under `kube/creds`, then merging only requested contexts in memory, including:

- The referenced cluster
- The referenced auth info
- `current-context`

Reasons:

- It can return only requested credentials (`cred_id` filtered).
- It avoids exposing unrelated targets.
- Gateway can use the result directly to build a `client-go` `rest.Config`.

#### 5.3 Network and Security Boundary

V1 assumes:

- Gateway and Jump Pod Agent are in the same Kubernetes network.
- Gateway reaches Agent through pod DNS or a stable service.
- Agent does not add application-layer authentication; caller isolation relies on internal networking and NetworkPolicy.

This keeps V1 simple but depends on correct network isolation.

### 6. Interface and Type Additions

Expected additions:

#### 6.1 DevOps HTTP API

- `CreateAuthorizationReq.enable_gateway_access` (default `false`)
- API to generate Gateway kubeconfig and JWT
- Internal JWKS API

#### 6.2 Jump Pod Agent HTTP API

- `GET /kubernetes/credential?cred_id=<cred_id>`
- Supports multiple `cred_id` query parameters in one request

#### 6.3 Internal Gateway Types

- Not added in current codebase yet

#### 6.4 Configuration

- Reuse existing DevOps internal token config:
  - `services.internalToken.signingKeyPEM`
  - `services.internalToken.keyID`
  - `services.internalToken.defaultTTL`
- Add jump gateway client config:
  - `services.jump.gateway.accessURL`
  - `services.jump.gateway.audience`

#### 6.5 DevOps Internal/DB Types

- `CreateAuthorizationParam.EnableGatewayAccess`
- `Authorization.EnableGatewayAccess` in entity/model
- `jump_authorizations.enable_gateway_access` with default `false`
- Gateway RBAC naming rule: `jump-gateway-<id>` where `<id>` is the same naming ID source as current `jump-<id>` resources

### 7. Compatibility

#### 7.1 Existing Jump Flow

- Keep authorization creation, approval, and state transitions unchanged.
- Keep existing RBAC and token injection behavior as default path.
- Add conditional Gateway-only read-only RBAC provisioning in `PrepareJump()` when `EnableGatewayAccess=true`.
- Keep the existing `cred_id` rule unchanged.

#### 7.2 Existing Jump Pod Usage

- Existing jump server / Jump Pod access remains available.
- Gateway is an additional access path, not a replacement.

#### 7.3 Upgrade and Downgrade

- If Gateway is not deployed, the existing flow is unaffected.
- If DevOps does not expose Gateway kubeconfig/JWT generation, existing APIs and Jump Pod behavior are unaffected.
- The new Agent read API does not affect `rebuild` or tailscale APIs.

## Test Design

### Functional Tests

- Create authorization with `enable_gateway_access=false`: only `jump-<id>` resources are created.
- Create authorization with `enable_gateway_access=true`: `jump-gateway-<id>` SA/ClusterRole/ClusterRoleBinding are created with read-only rules.
- Create authorization with `enable_gateway_access=false`: only primary Kubernetes credential `<type>.<id>` is generated and stored.
- Create authorization with `enable_gateway_access=true`: both primary credential `<type>.<id>` and gateway credential `gateway/<type>.<id>` are generated and stored.
- Refresh jump with `k8s.enable_gateway_access=true`: both primary and gateway credentials are refreshed.
- Finalize jump: both primary and gateway credentials are removed.
- Retry/sync same authorization: gateway resources remain idempotent without duplication errors.
- DevOps generates one token and multiple contexts for a multi-destination authorization.
- Gateway verifies DevOps-issued JWTs through JWKS.
- Gateway successfully proxies requests for valid JWT and `cred_id`.
- Gateway rejects requests with:
  - Missing Authorization header
  - Invalid signature
  - Mismatched `iss` or `aud`
  - Expired token
  - A path `cred_id` not listed in `destinations`
- Jump Pod Agent returns a kubeconfig that `client-go` can load.
- Jump Pod Agent returns `404` for unknown `cred_id`.

### Scenario Tests

- One authorization with multiple Kubernetes destinations lets the client switch contexts across clusters.
- Cancelled/expired authorization cleans up `jump-gateway-<id>` with primary jump resources.
- Refresh flow preserves dual-credential behavior by reading `k8s.enable_gateway_access` from jump attributes.
- Enabling gateway access does not change JWKS endpoint path or token signing constraints.
- Gateway rejects the JWT after authorization expiration reaches token `exp`.
- If a target kubeconfig is not ready in the Jump Pod, Gateway returns a clear non-permission error.
- After restart, Gateway reloads JWKS and continues validating old and new tokens.
- Concurrent requests to the same `cred_id` remain stable during fetch and proxy.

### Compatibility Tests

- Existing jump server / Jump Pod access remains available.
- Existing authorization approval flow remains unchanged, and `PrepareJump` adds only conditional Gateway read-only RBAC when enabled.
- Primary `cred_id` still uses `destination_type.destination_id`; Gateway uses `gateway/<destination_type>.<destination_id>`.
- Existing Kubernetes destination types such as Dedicated, Premium, Premium Resource Pool, BYOC, Serverless, and Control Plane remain compatible.
- The new Agent version does not require changes to existing `rebuild` callers.

## Impacts & Risks

### Impacts

- Adds a Kubernetes access path that does not require logging in to the Jump Pod.
- Reuses the current approval and RBAC injection pipeline.
- Creates a single Gateway entry point that can later support centralized auditing, caching, quotas, and rate limiting.
- Adds one new server plus JWT/JWKS configuration and one extra hop to the request path.

### Risks

- JWTs remain usable until expiration because V1 has no online revocation.
- Agent relies on network isolation instead of application-layer authentication.
- Gateway cannot proxy if the Jump Pod is not ready or the target credential is missing.
- Fetching kubeconfig per request may add latency if no cache is introduced.
- Signing keys, JWKS, Gateway deployment, and certificates increase operational complexity.

## Unresolved Questions

- Should Gateway keep a short-lived local kubeconfig cache, and how should it expire?
- Should Jump Pod Agent add application-layer authentication in a later version?
- Should Gateway audit logs eventually be sent to a centralized audit system?
- Should high-risk scenarios support online revocation or a revocation list?

## Assumptions

- Gateway and Jump Pod Agent are in the same Kubernetes/VPC network and can communicate directly.
- V1 accepts short-lived JWTs plus natural expiration as the revocation model.
- DevOps owns the JWT private key, and Gateway consumes only the public key or JWKS.
- This document stays at implementation-design level and does not expand into a file-by-file change list.
