Skip to content

Commit 7d82f45

Browse files
committed
docs: distinguish automatic and explicit API credentials
Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
1 parent 451a2fc commit 7d82f45

5 files changed

Lines changed: 97 additions & 27 deletions

File tree

‎crates/registry-cli-api/README.md‎

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,13 @@ as its maintained example.
2727
Connection selection does not create product authority. `--client` delegates
2828
to the product adapter, which owns any retained local endpoint, credential,
2929
profile headers, and readiness checks. `--connection` materializes one saved
30-
connection for that product, and `--url` is an advanced one-off destination
31-
that is anonymous unless `--token-file` is supplied. Saved authentication is
32-
exactly one of anonymous, a referenced bearer, or `private_key_jwt`. The
33-
receiving product still authenticates and authorizes every protected request.
30+
connection for that product. `--url` is an advanced one-off destination with
31+
no automatic credential unless `--token-file` is supplied. A saved
32+
connection's automatic authentication mode is one of anonymous, a referenced
33+
bearer, or `private_key_jwt`. Anonymous means that the connection supplies no
34+
automatic credential; when no automatic provider is selected, the raw command
35+
may still supply one explicit `Authorization` header. The receiving product
36+
still authenticates and authorizes every protected request.
3437

3538
The direct `--token-file` input must fit within 8 KiB, including its optional
3639
line ending. That ending is removed and the bearer remains opaque; it need not
@@ -43,19 +46,33 @@ the same reader under its own document bound. Saved bearer and private-key
4346
references instead use the platform secret resolver, retaining that resolver's
4447
separate root, permission, size, and symlink policy.
4548

49+
An explicit header may be inline, read as one value with `NAME: @FILE`, or
50+
read as complete `NAME: VALUE` lines with `@FILE`. Each header file is bounded
51+
to 64 KiB. These explicit headers are raw request material held as ordinary
52+
strings; they do not inherit the 8 KiB `--token-file` credential contract or
53+
the saved secret resolver's custody policy. In particular, an inline
54+
credential may be visible to process-argument inspection.
55+
4656
Every service base URL uses the shared checked HTTP(S) type, which permits
4757
plain HTTP only for loopback. A request path stays under the selected origin
4858
and deployment prefix. The selected connection owns `Host`, automatic
49-
`Authorization`, and any product profile header it sets, so command-line headers
50-
cannot replace them. The client ignores ambient proxies, follows no redirect,
51-
retries no request, applies fixed connect and overall deadlines, and bounds
52-
both request and response bodies. Debug output and transport errors do not
53-
contain the bearer or request values.
59+
`Authorization`, and any product profile header it sets, so command-line
60+
headers cannot replace them. Without an automatic provider, exactly one
61+
explicit `Authorization` header is accepted through any of the raw header
62+
forms; another explicit source is refused. The client ignores ambient proxies,
63+
follows no redirect, retries no request, applies fixed connect and overall
64+
deadlines, and bounds both request and response bodies. `ApiArgs` debug output
65+
records header counts and option presence rather than raw request values, and
66+
the automatic bearer connection's tested debug and failure paths do not expose
67+
that bearer. Those focused properties are not a general secret-custody
68+
guarantee for explicit raw headers.
5469

5570
The maintained proofs are:
5671

5772
- `crates/registry-cli-api/tests/api_http.rs`, including
73+
`anonymous_requests_accept_one_explicit_authorization_header_from_each_supported_input`,
5874
`automatic_credentials_cannot_be_replaced_by_a_repeated_header`,
75+
`api_args_debug_redacts_request_values`,
5976
`paths_and_host_headers_cannot_escape_the_selected_endpoint`,
6077
`redirect_answers_are_returned_without_following_them`,
6178
`response_bodies_are_bounded`, and

‎crates/registry-cli-api/tests/api_http.rs‎

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,50 @@ fn automatic_credentials_cannot_be_replaced_by_a_repeated_header() {
120120
assert!(!error.to_string().contains("canary-token"));
121121
}
122122

123+
#[test]
124+
fn anonymous_requests_accept_one_explicit_authorization_header_from_each_supported_input() {
125+
let temporary = TempDir::new().unwrap();
126+
let complete_header = temporary.path().join("complete-header");
127+
let header_value = temporary.path().join("header-value");
128+
fs::write(
129+
&complete_header,
130+
"Authorization: Bearer complete-file-canary\n",
131+
)
132+
.unwrap();
133+
fs::write(&header_value, "Bearer value-file-canary\n").unwrap();
134+
135+
for (argument, expected) in [
136+
(
137+
"Authorization: Bearer inline-canary".to_owned(),
138+
"Bearer inline-canary",
139+
),
140+
(
141+
format!("@{}", complete_header.display()),
142+
"Bearer complete-file-canary",
143+
),
144+
(
145+
format!("Authorization: @{}", header_value.display()),
146+
"Bearer value-file-canary",
147+
),
148+
] {
149+
let response = b"HTTP/1.1 200 OK\r\nContent-Length: 2\r\nConnection: close\r\n\r\nok";
150+
let (base_url, request) = server(response);
151+
let mut args = api_args("v1/status", temporary.path().join("out"));
152+
args.url = Some(base_url);
153+
args.header = vec![argument];
154+
run_api("breg", args, |_, _| unreachable!("--url bypasses resolver")).unwrap();
155+
156+
let request = String::from_utf8(request.recv().unwrap()).unwrap();
157+
let authorization = request
158+
.lines()
159+
.filter_map(|line| line.split_once(':'))
160+
.filter(|(name, _)| name.eq_ignore_ascii_case("authorization"))
161+
.map(|(_, value)| value.trim_start())
162+
.collect::<Vec<_>>();
163+
assert_eq!(authorization, [expected]);
164+
}
165+
}
166+
123167
#[test]
124168
fn local_connection_headers_cannot_be_replaced_by_cli_headers() {
125169
let temporary = TempDir::new().unwrap();

‎products/coordinator/SECURITY-REVIEW-NOTES.md‎

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -92,14 +92,17 @@ workflow authority by itself.
9292

9393
Coordinator retains no local caller session, so `coordinatorctl api` refuses
9494
`--client`. The operator selects a saved connection or an explicit one-off
95-
URL. Saved authentication is exactly one of anonymous, a referenced opaque
96-
bearer, or renewable `private_key_jwt`, under the shared
95+
URL. A saved connection's automatic authentication is exactly one of
96+
anonymous, a referenced opaque bearer, or renewable `private_key_jwt`, under
97+
the shared
9798
[`registry-cli-api` security boundary](../../crates/registry-cli-api/README.md#security-boundary).
98-
The helper confines the destination and headers, acquires the selected
99-
credential, and releases only the raw bounded response. It carries no caller
100-
policy, workflow, run ownership, or downstream product authority. Coordinator
101-
still authenticates and authorizes each protected route, and every receiving
102-
product still decides an effect requested by a run.
99+
Anonymous supplies no automatic credential, but the raw command may still
100+
supply one explicit `Authorization` header. The helper confines the
101+
destination and headers, acquires any selected automatic credential, and
102+
releases only the raw bounded response. It carries no caller policy, workflow,
103+
run ownership, or downstream product authority. Coordinator still
104+
authenticates and authorizes each protected route, and every receiving product
105+
still decides an effect requested by a run.
103106

104107
This path deliberately does not reuse the legacy reader described above.
105108
Legacy authenticated Coordinator commands keep their 16,384-byte compact-JWT

‎products/discovery/README.md‎

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -43,11 +43,13 @@ then restart. The runtime verifies `SHA256SUMS` and an optional
4343

4444
`discoveryctl api` has no local `--client` credential adapter. Discovery's
4545
endpoint is public, so the product resolver directs operators to an anonymous
46-
saved connection. The shared CLI also permits an operator to deliberately
47-
select an explicit or saved authenticated connection, but that credential is
48-
operator-supplied transport state: it is never derived from catalog metadata,
49-
creates no Discovery authority, and does not make Discovery a native-provider
50-
proxy. Endpoint and request handling follow the shared
46+
saved connection, meaning the connection supplies no automatic credential.
47+
The shared CLI also permits one explicit `Authorization` header on that raw
48+
request, or lets an operator deliberately select an explicit or saved
49+
authenticated connection. Such a credential is operator-supplied transport
50+
state: it is never derived from catalog metadata, creates no Discovery
51+
authority, and does not make Discovery a native-provider proxy. Endpoint and
52+
request handling follow the shared
5153
[`registry-cli-api` security boundary](../../crates/registry-cli-api/README.md#security-boundary).
5254
`api_and_saved_connections_are_available_without_a_runtime_config` in
5355
`crates/registry-discoveryctl/tests/api_cli.rs` pins the product surface; the

‎products/scheduling/SECURITY-REVIEW-NOTES.md‎

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1501,15 +1501,19 @@ a credential selected for one connection is silently replaced on the request.
15011501

15021502
Scheduling retains no local caller session, so `schedulingctl api` does not
15031503
resolve `--client`. The operator selects a saved connection or an explicit
1504-
one-off URL. A saved connection may be anonymous, a referenced bearer, or a
1505-
renewable `private_key_jwt` client under the shared
1504+
one-off URL. A saved connection's automatic authentication may be anonymous,
1505+
a referenced bearer, or a renewable `private_key_jwt` client under the shared
15061506
[`registry-cli-api` security boundary](../../crates/registry-cli-api/README.md#security-boundary).
1507-
That boundary owns endpoint confinement, automatic authorization, credential
1508-
resolution, redirects, proxies, bounds, and redaction; it carries no task
1509-
grant, catalogue scope, appointment ownership, or capacity authority.
1507+
That boundary owns endpoint confinement, automatic credential resolution and
1508+
header precedence, redirects, proxies, bounds, and its focused debug
1509+
redaction; it carries no task grant, catalogue scope, appointment ownership,
1510+
or capacity authority.
15101511

15111512
The runtime continues to authenticate and authorize every protected request.
1512-
An anonymous connection reaches only routes the runtime already makes public.
1513+
An anonymous connection supplies no automatic credential, but the raw command
1514+
may still supply one explicit `Authorization` header under the shared contract.
1515+
The runtime, not the connection label, decides whether the request may reach a
1516+
public or protected route.
15131517
`api_and_saved_connections_are_available_without_a_runtime_config` and
15141518
`raw_api_refuses_report_format_before_network_access` in
15151519
`crates/registry-schedulingctl/tests/api_cli.rs` pin the product surface. The

0 commit comments

Comments
 (0)