Skip to content

Commit c531751

Browse files
committed
Restructure RFC: Part 1 BOSH-only, CC API content in Part 2
- Architecture Overview gains end-to-end step table labelled by part - Part 1 now contains only the GoRouter BOSH configuration (mtls_domains); no Cloud Controller API changes belong here - CC API domain settings (enforce_access_rules, access_rules_scope, scope table, shared routes / endpoint pools, operator CLI commands) moved to the top of Part 2 - Part 2 opening sentence makes the boundary explicit: implemented entirely through CC API changes, no additional BOSH config needed - xfcc_format yaml comment trimmed; new XFCC header format paragraph added after the BOSH block with link to Envoy XFCC docs on the envoy bullet - Various fixes from PR review: selector_resource_guids filter, GUID validation clarification, --path flag on all CLI commands, cf:any mutual exclusivity, enable/disable-domain-access-rules separate commands, API endpoints table expanded, cascade delete and metadata
1 parent d780f8f commit c531751

1 file changed

Lines changed: 119 additions & 42 deletions

File tree

toc/rfc/rfc-draft-domain-scoped-mtls-gorouter.md

Lines changed: 119 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,21 @@ GoRouter gains the ability to require client certificates for specific domains,
4343

4444
### Architecture Overview
4545

46-
The diagram below shows CF app-to-app routing (the most complex use case). For external client certificate validation, only GoRouter and the backend app are involved—external clients connect directly to GoRouter with their certificates.
46+
**How it works end-to-end:**
47+
48+
| Step | Part | Actor | What happens |
49+
|------|------|-------|--------------|
50+
| 1 | 1 | Operator | Configures a domain with mTLS requirements in the `mtls_domains` BOSH configuration |
51+
| 2 | 1 | DNS | BOSH DNS (or external DNS) resolves the domain to GoRouter instances |
52+
| 3 | 1 | Developer | Maps application routes to this domain like any shared domain |
53+
| 4 | 1 | GoRouter | Requires and validates a client certificate, sets the XFCC header |
54+
| 5 | 2 | Operator | Enables access rules enforcement on the domain via the CF API (`enforce_access_rules: true`) |
55+
| 6 | 2 | Developer | Creates access rules per route via the Access Rules API |
56+
| 7 | 2 | GoRouter | Extracts CF identity from the certificate and enforces access rules |
57+
58+
Part 1 alone (without Part 2) is sufficient for external client certificate validation: GoRouter validates the cert and sets the XFCC header; backend applications handle authorization themselves based on that header.
59+
60+
The diagram below shows the most complex use case: CF app-to-app routing with both parts active.
4761

4862
```mermaid
4963
flowchart LR
@@ -70,18 +84,7 @@ flowchart LR
7084

7185
### Part 1: mTLS Domain Infrastructure
7286

73-
GoRouter gains the ability to require client certificates for specific domains while leaving other domains unaffected. This infrastructure is generic and can be used for multiple purposes beyond CF app-to-app routing.
74-
75-
**How it works:**
76-
1. Operator configures a domain with mTLS requirements in the `mtls_domains` BOSH configuration
77-
2. Operator enables access rules enforcement via Cloud Controller API (`enforce_access_rules: true`)
78-
3. DNS (BOSH DNS or external) resolves the domain to GoRouter instances
79-
4. Applications map routes to this domain like any shared domain
80-
5. When a client connects, GoRouter:
81-
- Requires a client certificate
82-
- Validates it against the configured CA
83-
- Sets the XFCC header with certificate details (format configurable)
84-
- If route options contain `access_rules`, enforces authorization (Part 2)
87+
GoRouter gains the ability to require client certificates for specific domains while leaving other domains unaffected. This infrastructure is generic and can be used for multiple purposes beyond CF app-to-app routing. Operators configure it entirely through the BOSH manifest.
8588

8689
**GoRouter BOSH Configuration:**
8790

@@ -100,26 +103,32 @@ router:
100103
# always_forward - Always pass through, even if no client cert
101104
forwarded_client_cert: sanitize_set
102105

103-
# Format of the XFCC header:
104-
# raw (default) - Full base64-encoded certificate (~1.5KB)
105-
# envoy - Compact format (~300 bytes):
106-
# Hash=<sha256>;Subject="CN=<instance-id>,OU=app:<guid>..."
107-
# When using envoy format, GoRouter automatically extracts
108-
# CF identity (app/space/org GUIDs) from the Subject OU fields
109-
# for authorization enforcement.
106+
# Format of the XFCC header value: raw (default) or envoy
110107
xfcc_format: envoy
111108
```
112109
113-
The BOSH configuration is minimal—just TLS settings. Authorization policy is managed entirely through Cloud Controller.
110+
**XFCC header format:**
111+
112+
The `xfcc_format` field controls the format of the `X-Forwarded-Client-Cert` header that GoRouter sets on proxied requests:
113+
114+
- **`raw`** (default): The full PEM certificate is base64-encoded and placed in the header. This produces a large header value (approximately 1.5 KB per certificate) that the backend application must decode and parse to extract identity fields.
115+
- **`envoy`**: GoRouter uses the same compact format as [Envoy's XFCC implementation](https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_conn_man/headers#x-forwarded-client-cert): `Hash=<sha256-fingerprint>;Subject="<distinguished-name>"`. This reduces the header to approximately 300 bytes. When `xfcc_format: envoy` is configured and Part 2 authorization is active, GoRouter parses identity directly from the Subject's `OU` fields (`OU=app:<guid>`, `OU=space:<guid>`, `OU=organization:<guid>`) without decoding the full certificate, which is more efficient.
116+
117+
### Part 2: CF Identity & Authorization
114118

115-
**Cloud Controller Domain Configuration:**
119+
Part 2 adds Cloud Foundry identity and authorization on top of the mTLS infrastructure from Part 1. It is implemented entirely through Cloud Controller API changes — no additional BOSH configuration is required beyond Part 1.
116120

117-
Operators enable access rules enforcement via the Cloud Controller API:
121+
**Operator configuration (CC API):**
122+
123+
Operators enable access rules enforcement on a domain via the Cloud Controller API:
118124

119125
```bash
120-
# Enable access rules enforcement on a domain
126+
# Enable access rules enforcement with org scope
121127
cf enable-domain-access-rules apps.mtls.internal --scope org
122128
129+
# Disable access rules enforcement
130+
cf disable-domain-access-rules apps.mtls.internal
131+
123132
# Or via API
124133
cf curl -X PATCH /v3/domains/domain-guid -d '{
125134
"enforce_access_rules": true,
@@ -158,8 +167,6 @@ EndpointPool for "api.apps.mtls.internal":
158167

159168
GoRouter iterates the pool's endpoints when evaluating scope, checking the caller's identity against each endpoint's tags and short-circuiting on the first match. For `scope: space`, if any endpoint's `space_id` matches the caller's space GUID, the request is allowed. A caller from Space A or Space B passes the check; a caller from Space C (with no app mapped to the route) is denied. This naturally enables cross-space access on shared routes between the participating spaces.
160169

161-
### Part 2: CF Identity & Authorization
162-
163170
When a domain has `enforce_access_rules: true`, GoRouter enforces access control at the routing layer using a default-deny model, matching the design of container-to-container network policies. If no access rules are configured for a route, all requests are denied.
164171

165172
**Identity extraction:** GoRouter extracts CF identity from Diego instance identity certificates regardless of `xfcc_format`. With `envoy` format, identity is parsed from pre-extracted Subject fields (`OU=app:<guid>,OU=space:<guid>,OU=organization:<guid>`). With `raw` format, GoRouter decodes the base64 certificate and extracts the same fields. The `envoy` format is more performant but both work identically for authorization.
@@ -182,16 +189,19 @@ Developers can only **restrict further** within operator boundaries. They cannot
182189

183190
#### Access Rules API
184191

185-
Developers manage route-level access rules through a dedicated Cloud Controller API. Each access rule has a human-readable name for auditability and a selector that identifies allowed callers. See [Access Rules API Examples](#access-rules-api-examples) for full request/response examples.
192+
Developers manage route-level access rules through a dedicated Cloud Controller API. Each access rule has a human-readable name for auditability and a selector that identifies allowed callers. Access rules are owned by their route: deleting a route cascades to delete all its access rules. See [Access Rules API Examples](#access-rules-api-examples) for full request/response examples.
186193

187194
**API Endpoints:**
188195

189196
| Method | Path | Description |
190197
|--------|------|-------------|
191198
| `GET` | `/v3/access_rules` | List access rules (with filters) |
192-
| `GET` | `/v3/routes/:route_guid/access_rules` | List access rules for a route |
193-
| `POST` | `/v3/routes/:route_guid/access_rules` | Create access rules |
199+
| `GET` | `/v3/access_rules/:guid` | Get a single access rule |
200+
| `POST` | `/v3/access_rules` | Create an access rule |
201+
| `PATCH` | `/v3/access_rules/:guid` | Update an access rule (metadata only) |
194202
| `DELETE` | `/v3/access_rules/:guid` | Delete a rule by guid |
203+
| `GET` | `/v3/routes/:route_guid/access_rules` | List access rules for a route |
204+
| `POST` | `/v3/routes/:route_guid/access_rules` | Create access rules for a route (batch convenience endpoint) |
195205

196206
**List Query Parameters:**
197207

@@ -200,6 +210,7 @@ Developers manage route-level access rules through a dedicated Cloud Controller
200210
| `names` | Comma-delimited list of rule names to filter by |
201211
| `route_guids` | Comma-delimited list of route guids to filter by |
202212
| `selectors` | Comma-delimited list of selectors to filter by |
213+
| `selector_resource_guids` | Comma-delimited list of resolved selector resource GUIDs to filter by. Pass empty value (`selector_resource_guids=`) to return only rules whose selector resource no longer exists (stale rules). |
203214
| `include` | Comma-delimited list of related resources to include: `route`, `selector_resource` |
204215
| `page` | Page to display (default: 1) |
205216
| `per_page` | Number of results per page (default: 50) |
@@ -249,28 +260,27 @@ The `cf:` prefix is reserved for Cloud Foundry native identities. Future extensi
249260
cf add-access-rule frontend-app apps.mtls.internal cf:app:d76446a1-f429-4444-8797-be2f78b75b08 \
250261
--hostname backend
251262
263+
# Add an access rule for a route with a path
264+
cf add-access-rule frontend-app apps.mtls.internal cf:app:d76446a1-f429-4444-8797-be2f78b75b08 \
265+
--hostname backend --path /api
266+
252267
# List access rules
253268
cf access-rules apps.mtls.internal --hostname backend
269+
cf access-rules apps.mtls.internal --hostname backend --path /api
254270
255271
# Remove an access rule
256272
cf remove-access-rule frontend-app apps.mtls.internal --hostname backend
273+
cf remove-access-rule frontend-app apps.mtls.internal --hostname backend --path /api
257274
```
258275

259-
**Domain-level configuration (Admins for shared domains, Org Managers for private domains):**
260-
261-
```bash
262-
# Enable access rules enforcement with org scope
263-
cf enable-domain-access-rules apps.mtls.internal --scope org
264-
265-
# Disable access rules enforcement
266-
cf enable-domain-access-rules apps.mtls.internal --disable
267-
```
276+
Selectors always require a GUID rather than a name. This is intentional: the person creating the rule does not need read access to the selector's source app, space, or org. The GUID is a public identity that the calling team shares out of band.
268277

269278
**Validation rules:**
270279
- Access rules can only be created for routes on domains where `enforce_access_rules` is true
271-
- `cf:any` is mutually exclusive with specific selectors on the same route (cannot combine `cf:any` with `cf:app:*`)
280+
- `cf:any` cannot be combined with other selectors on the same route. If a route has a `cf:any` rule, no other rules (`cf:app:...`, `cf:space:...`, `cf:org:...`) can be added.
272281
- Duplicate selectors on the same route are rejected with an error
273282
- Rule names must be unique per route
283+
- Selector GUIDs (`cf:app:<guid>`, `cf:space:<guid>`, `cf:org:<guid>`) are not validated against Cloud Controller at creation time. The developer creating the rule does not need visibility into the source app, space, or org. App and space GUIDs function as public identities that teams can share with each other out of band. This intentionally differs from C2C networking, where both sides of a policy must be reachable by the policy creator.
274284

275285
**Authorization:**
276286

@@ -284,7 +294,7 @@ Cloud Controller stores access rules in a dedicated `route_access_rules` table.
284294
# Access rules are converted to RFC-0027 compliant route options
285295
route.options = {
286296
"access_scope": "org",
287-
"access_rules": "cf:app:guid1,cf:space:guid2,cf:any"
297+
"access_rules": "cf:app:guid1,cf:space:guid2"
288298
}
289299
```
290300

@@ -364,7 +374,50 @@ This preserves backward compatibility with existing deployments while preventing
364374

365375
### Access Rules API Examples
366376

367-
**Create Request:**
377+
**Create a single rule (`POST /v3/access_rules`):**
378+
379+
```http
380+
POST /v3/access_rules
381+
```
382+
383+
```json
384+
{
385+
"name": "frontend-app",
386+
"selector": "cf:app:d76446a1-f429-4444-8797-be2f78b75b08",
387+
"metadata": {
388+
"labels": { "team": "payments" },
389+
"annotations": { "description": "Allow frontend to call payments API" }
390+
},
391+
"relationships": {
392+
"route": { "data": { "guid": "route-guid" } }
393+
}
394+
}
395+
```
396+
397+
**Create Response:**
398+
399+
```json
400+
{
401+
"guid": "rule-guid-1",
402+
"name": "frontend-app",
403+
"selector": "cf:app:d76446a1-f429-4444-8797-be2f78b75b08",
404+
"created_at": "2026-03-25T10:00:00Z",
405+
"updated_at": "2026-03-25T10:00:00Z",
406+
"metadata": {
407+
"labels": { "team": "payments" },
408+
"annotations": { "description": "Allow frontend to call payments API" }
409+
},
410+
"relationships": {
411+
"route": { "data": { "guid": "route-guid" } }
412+
},
413+
"links": {
414+
"self": { "href": "/v3/access_rules/rule-guid-1" },
415+
"route": { "href": "/v3/routes/route-guid" }
416+
}
417+
}
418+
```
419+
420+
**Batch create for a route (`POST /v3/routes/:route_guid/access_rules`):**
368421

369422
```http
370423
POST /v3/routes/:route_guid/access_rules
@@ -385,7 +438,7 @@ POST /v3/routes/:route_guid/access_rules
385438
}
386439
```
387440

388-
**Create Response:**
441+
**Batch Create Response:**
389442

390443
```json
391444
{
@@ -396,6 +449,10 @@ POST /v3/routes/:route_guid/access_rules
396449
"selector": "cf:app:d76446a1-f429-4444-8797-be2f78b75b08",
397450
"created_at": "2026-03-25T10:00:00Z",
398451
"updated_at": "2026-03-25T10:00:00Z",
452+
"metadata": {
453+
"labels": {},
454+
"annotations": {}
455+
},
399456
"relationships": {
400457
"route": { "data": { "guid": "route-guid" } }
401458
},
@@ -408,6 +465,22 @@ POST /v3/routes/:route_guid/access_rules
408465
}
409466
```
410467

468+
**Update metadata (`PATCH /v3/access_rules/:guid`):**
469+
470+
```http
471+
PATCH /v3/access_rules/rule-guid-1
472+
```
473+
474+
```json
475+
{
476+
"metadata": {
477+
"labels": { "team": "payments", "env": "prod" },
478+
"annotations": { "description": "Updated description" }
479+
}
480+
}
481+
```
482+
```
483+
411484
**List with includes (for auditing):**
412485

413486
```http
@@ -431,6 +504,10 @@ GET /v3/access_rules?include=route,selector_resource
431504
"selector": "cf:app:d76446a1-f429-4444-8797-be2f78b75b08",
432505
"created_at": "2026-03-25T10:00:00Z",
433506
"updated_at": "2026-03-25T10:00:00Z",
507+
"metadata": {
508+
"labels": { "team": "payments" },
509+
"annotations": {}
510+
},
434511
"relationships": {
435512
"route": { "data": { "guid": "route-guid" } }
436513
},

0 commit comments

Comments
 (0)