Skip to content

Commit dd14e6d

Browse files
authored
Merge pull request opensandbox-group#754 from Pangjiping/docs/update-osep-status
FEATURE: OSEP-0011 for secure access endpoint
2 parents b0e12dd + 7517a8b commit dd14e6d

2 files changed

Lines changed: 147 additions & 4 deletions

File tree

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
---
2+
title: Secure Access on GetEndpoint and Signed Endpoint
3+
authors:
4+
- "@Pangjiping"
5+
creation-date: 2026-04-19
6+
last-updated: 2026-04-20
7+
status: draft
8+
---
9+
10+
# OSEP-0011: Secure Access on GetEndpoint and Signed Endpoint
11+
12+
## Summary
13+
14+
Optional `secure_access` on sandbox create. **`GetSignedEndpoint(sandboxId, port)`** returns a URL that embeds a **route `signature`** (a **10-character** token). There is **no** `expires`, **no** signing of app path or query, and **no** DNS parent domain in the signed material. The wildcard parent domain is **routing-only**.
15+
16+
The **`signature`** is:
17+
18+
1. **`hex8`** — first **8** characters of the lowercase hex encoding of **`SHA256(inner)`** (i.e. the first **4** bytes of the digest as hex).
19+
2. **`signed_key_id`** — the **last 2** characters of **`signature`**, **`[0-9a-z]`**, equal to the **`key_id`** of the `secret_bytes` row used to mint (typically the server **`active_key`**).
20+
21+
`GetEndpoint` may still return an opaque static **`OPENSANDBOX-SECURE-ACCESS`** header value (annotation / access token) when enabled. That header path is **separate** from the route **`signature`**.
22+
23+
## Signing algorithm (implementation order)
24+
25+
### 1) Inputs and constraints
26+
27+
- **`sandbox_id`**: used verbatim in canonical (may contain `-`).
28+
- **`port`**: decimal integer in **`1..65535`**, **no leading zeros** (e.g. `08080` is invalid).
29+
- **`secret_bytes`**: raw decoded secret bytes for the chosen **`signed_key_id`** (same material ingress uses to verify).
30+
31+
### 2) Build `canonical_bytes` (UTF-8)
32+
33+
Concatenate **exactly** in this order, using a single **LF** (`\n`) between segments:
34+
35+
```text
36+
v1\nshort\n{sandbox_id}\n{port}\n
37+
```
38+
39+
Equivalent explicit concatenation:
40+
41+
```text
42+
"v1" + "\n" + "short" + "\n" + sandbox_id + "\n" + decimal(port) + "\n"
43+
```
44+
45+
### 3) Build `inner` (length-prefixed byte concatenation)
46+
47+
`BE32(x)` is **4** bytes, **big-endian** unsigned 32-bit integer **`x`**.
48+
49+
```text
50+
inner = BE32(len(secret_bytes))
51+
|| secret_bytes
52+
|| BE32(len(canonical_bytes))
53+
|| canonical_bytes
54+
```
55+
56+
### 4) Hash and mint `signature`
57+
58+
```text
59+
digest = SHA256(inner) // 32 bytes
60+
hex_all = lowercase_hex(digest) // 64 chars
61+
hex8 = hex_all[0:8]
62+
signature = hex8 + signed_key_id // 10 chars total
63+
```
64+
65+
> The signature binds **`sandbox_id`**, **`port`**, and the signing key only — not the gateway hostname or DNS suffix.
66+
67+
## API
68+
69+
- **CreateSandbox:** `secure_access.enabled` (default `false`).
70+
- **GetSignedEndpoint(sandboxId, port):** returns `signed_endpoint` consistent with `[ingress.gateway].route.mode`, embedding **`signature`**.
71+
72+
## Gateway routing (where the credential lives)
73+
74+
### Host / header token (split on `-` from the **right**)
75+
76+
- **Three or more segments** `<sandbox-id>-<port>-<signature>`:
77+
- **Last** segment: **`signature`** (must match **`[0-9a-f]{8}[0-9a-z]{2}`**).
78+
- **Second-to-last**: **`port`** (rules above).
79+
- **Everything before** (re-joined with `-`): **`sandbox_id`**.
80+
- **Two segments** `<sandbox-id>-<port>`: **unsigned** route; **`signature`** is empty (legacy compatibility).
81+
82+
| Mode | Where |
83+
|------|-------|
84+
| **Wildcard** | Host: `{sandbox_id}-{port}-{signature}.<parent-domain>` (parent domain from gateway DNS only; not signed) |
85+
| **Header** | Header value only: `{sandbox_id}-{port}-{signature}` |
86+
| **URI** | Path: `/{sandbox_id}/{port}/{signature}/` + remainder to upstream |
87+
88+
### URI parsing nuance
89+
90+
- If the path matches **OSEP** shape (valid **`port`** in segment 2 and a valid 10-char **`signature`** in segment 3), treat segments 1–3 as routing prefix and the rest as upstream path.
91+
- Otherwise parse as **legacy** URI: first segment = **`sandbox_id`**, second = **`port`**, remainder (if any) = upstream path — **no** embedded **`signature`**.
92+
- For sandboxes that **do not** require secure access, an OSEP-shaped path may be **reinterpreted** as legacy so a normal path segment is not mistaken for **`signature`**.
93+
94+
After successful authorization, strip the routing token from host / header / path prefix; forward the remaining path and query unchanged.
95+
96+
## Ingress verification
97+
98+
1. Parse **`sandbox_id`**, **`port`**, optional route **`signature`** from host, header, or URI (per mode).
99+
2. **`GetEndpoint(sandbox_id)`** — determine whether the sandbox requires secure access and obtain **`SecureAccessToken`** (annotation) if any.
100+
3. **Unified access decision:**
101+
- If the sandbox does **not** require secure access → allow.
102+
- If it **does** require secure access:
103+
- If **`OPENSANDBOX-SECURE-ACCESS`** is present → it **must** equal the sandbox token (constant-time compare) or **`401`**.
104+
- Else if route **`signature`** is present → rebuild **`canonical_bytes`**, recompute **`hex8`**, verify against **`secret_bytes`** for **`signed_key_id`** from **`--secure-access-keys`****`401`** on mismatch or unknown key.
105+
- Else **`401`** (signature required).
106+
107+
## Config
108+
109+
**Server (`~/.sandbox.toml`):**
110+
111+
```toml
112+
[ingress.secure_access]
113+
enabled = true
114+
active_key = "k1" # 2 chars, must exist in keys
115+
116+
[[ingress.secure_access.keys]]
117+
key_id = "k1"
118+
secret = "base64:..."
119+
120+
[[ingress.secure_access.keys]]
121+
key_id = "k0"
122+
secret = "base64:..."
123+
```
124+
125+
The server mints **`signature`** using **`secret_bytes`** for **`active_key`**.
126+
127+
**Ingress:**
128+
129+
```bash
130+
opensandbox-ingress --secure-access-enabled \
131+
--secure-access-keys "k1=base64:...,k0=base64:..."
132+
```
133+
134+
## Errors
135+
136+
- **`400`:** malformed route / token shape, invalid **`port`**, invalid **`signature`** charset or length.
137+
- **`401`:** bad **`hex8`**, unknown **`signed_key_id`**, missing credential when required, or secure-access header mismatch.
138+
- **GetSignedEndpoint:** `404` / `403` when sandbox is missing or secure access is disabled.
139+
140+
## Tests
141+
142+
- Unit: `inner` / `hex8`, right-split with hyphens in **`sandbox_id`**, two-segment unsigned host, URI OSEP vs legacy.
143+
- Integration: three route modes + one tampered hex → **`401`**.

oseps/README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,10 @@ This is the complete list of OpenSandbox Enhancement Proposals:
99
| [OSEP-0001](0001-fqdn-based-egress-control.md) | FQDN-based Egress Control | implemented | 2026-01-22 |
1010
| [OSEP-0002](0002-kubernetes-sigs-agent-sandbox-support.md) | kubernetes-sigs/agent-sandbox Support | implemented | 2026-01-23 |
1111
| [OSEP-0003](0003-volume-and-volumebinding-support.md) | Volume Support | implementing | 2026-02-11 |
12-
| [OSEP-0004](0004-secure-container-runtime.md) | Pluggable Secure Container Runtime Support | implementing | 2026-02-09 |
12+
| [OSEP-0004](0004-secure-container-runtime.md) | Pluggable Secure Container Runtime Support | implemented | 2026-02-09 |
1313
| [OSEP-0005](0005-client-side-sandbox-pool.md) | Client-Side Sandbox Pool | implementing | 2026-03-09 |
1414
| [OSEP-0006](0006-developer-console.md) | Developer Console for Sandbox Operations | implementable | 2026-03-06 |
1515
| [OSEP-0007](0007-fast-sandbox-runtime-support.md) | Fast Sandbox Runtime Support | provisional | 2026-02-08 |
16-
| [OSEP-0008](0008-pause-resume-rootfs-snapshot.md) | Pause and Resume via Rootfs Snapshot | draft | 2026-03-13 |
17-
| [OSEP-0009](0009-auto-renew-sandbox-on-ingress-access.md) | Auto-Renew Sandbox on Ingress Access | implemented | 2026-03-23 |
18-
| [OSEP-0010](0010-opentelemetry-instrumentation.md) | OpenTelemetry Metrics and Logs (execd, egress, and ingress) | implementing | 2026-04-12 |
16+
| [OSEP-0008](0008-pause-resume-rootfs-snapshot.md) | Pause and Resume via Rootfs Snapshot | implementing | 2026-03-13 |
17+
| [OSEP-0009](0009-auto-renew-sandbox-on-ingress-access.md) | Auto-Renew Sandbox on Ingress Access | implemented | 2026-03-23 |
18+
| [OSEP-0010](0010-opentelemetry-instrumentation.md) | OpenTelemetry Metrics and Logs (execd, egress, and ingress) | implementing | 2026-04-12 |

0 commit comments

Comments
 (0)