Skip to content

Commit fd39c86

Browse files
committed
Proposed design document for authentication rework
Our current implementation of login suffers from structural issues manifest in a variety of ways (see #3072), and is vastly incomplete when it comes to supporting hosts.toml properly. The core issue is that it predates these mechanisms, and has been architected with different requirements in mind that need to be re-evaluated today, especially in light of upcoming features like oauth device-flow. First and foremost though, it lacks a lot of clarity about what is supposed to happen when, in a context where a lot of legacy features have complex interactions are effects: - the insecure-registry flag - the "default" behaviors for localhost - the use of the docker credentials store to retrieve credentials ... to a point that even seasoned contributors are confused as to whether we should allow a scheme to be passed in front of a registry url when we do login, or what exactly should happen in term of downgrading protocols or implied port and schemes. Besides the bugs, reconciling these legacy features, the requirement to use docker credentials, and being able to actually use hosts.toml does require significant changes to our login and dockerconfigresolver code. Some of these details are outlined in #3265 - though this is not the place for a proper reference. This document offers solutions, and describes precisely what happens when and how things work together. It is not a replacement for the hosts.toml slapstick spec, but rather a description of what we want to achieve with the upcoming rewrite. While there is a lot of important details in there that should not be overlooked, the key parts of the proposal are the introduction of a new flag allowing to log in into endpoints, an experimental scheme for storing endpoint credentials, and clarification on the role and use of --insecure-registry flag. Signed-off-by: apostasie <[email protected]>
1 parent 678393e commit fd39c86

File tree

1 file changed

+334
-0
lines changed

1 file changed

+334
-0
lines changed

docs/dev/auth_design.md

Lines changed: 334 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,334 @@
1+
# [TEMP TITLE] Design document for registry resolution and authentication
2+
3+
## Preamble
4+
5+
nerdctl supports a set of mechanisms that allows users to control behavior
6+
with regard to registry resolution and authentication.
7+
8+
Generally speaking, and like most tools in the ecosystem, nerdctl strongly encourages
9+
the use of TLS for all communications, as plain http is widely considered insecure
10+
and outright dangerous to use, even in the most restricted and controlled contexts.
11+
12+
Nowadays, setting-up a TLS registry is very simple (thanks to letsencrypt),
13+
and configuring nerdctl to recognize self-signed certificates is also trivial.
14+
15+
Nevertheless, there are still ways to disable TLS certificate validation, or even
16+
force nerdctl to downgrade to plain http communication in certain circumstances.
17+
18+
Note that nerdctl stores and retrieve credentials using docker's credential store implementation,
19+
allowing for some level of interoperability between the docker cli and nerdctl.
20+
21+
Finally, thanks to the [hosts.toml mechanism](https://github.com/containerd/containerd/blob/main/docs/hosts.md),
22+
nerdctl can be instructed to _resolve_ a certain _registry namespace_ to a completely different
23+
_endpoint_, or set of _endpoints_, with fine-grain capabilities.
24+
25+
The interaction between these mechanisms is complex, and if you want to go beyond the simplest
26+
cases (eg: docker cli), you have to understand the implications.
27+
28+
This document purport to extensively cover these.
29+
30+
## Vocabulary
31+
32+
### Registry namespace
33+
34+
A registry namespace is the _host name and port_ that you use
35+
to tag your images with.
36+
37+
In the following example, the _registry namespace_ is `namespace.example:1234`
38+
39+
```bash
40+
nerdctl tag debian namespace.example:1234/my_debian
41+
nerdctl images
42+
```
43+
44+
If there is no specific (`hosts.toml`) configuration on your side, the _registry namespace_
45+
will "resolve" to the following http url: `https://namespace.example:1234/v2/`
46+
47+
The http server at that address will be used when you try to push, or pull, (or login), through a series
48+
of http requests.
49+
50+
Note that omitting a _registry namespace_ from your image name _implies_ that the
51+
_registry namespace_ is `docker.io`.
52+
53+
### Registry host / endpoint
54+
55+
... refers to a fully qualified http url that normally points to an actual, live http server,
56+
able to service the [distribution protocol](https://github.com/opencontainers/distribution-spec).
57+
58+
As mentioned above, you may configure a _registry namespace_ to resolve to different _registry endpoints_,
59+
each with their own set of _allowed capabilities_ (`resolve`, `pull`, and `push`).
60+
61+
What that means is that when you:
62+
```bash
63+
nerdctl pull namespace.example:1234/my_debian
64+
```
65+
66+
... the http endpoint being contacted may very well be `https://somethingelse.example:5678/v2`
67+
68+
### Capabilities
69+
70+
A _registry capability_ refers to a specific registry operation:
71+
- `resolve`: converting a tag (like `latest`) to a digest
72+
- `pull`: retrieving a certain image by digest
73+
- `push`: sending over a locally store image
74+
75+
These distinct capabilities imply different levels of trust.
76+
While it is possible to `pull` an image by digest from an untrusted source,
77+
it is a bad idea to use that same source to `resolve` a tag to a digest,
78+
and even worse to publish an image there.
79+
80+
Granting capabilities to specific _registry endpoints_ is something you control
81+
and decide.
82+
83+
## hosts.toml and registry resolution
84+
85+
In the simplest scenario, as indicated above, without any specific configuration,
86+
the _registry namespace_ `namespace.example:1234` will resolve to the _registry endpoint_
87+
`https://namespace.example:1234/v2/`.
88+
89+
This resolution mechanism can be controlled through the use of `hosts.toml` files.
90+
91+
Said files should be stored under:
92+
- `~/.config/containerd/certs.d/namespace.example:1234/hosts.toml` (for rootless)
93+
- `/etc/containerd/certs.d/namespace.example:1234/hosts.toml` (for rootful)
94+
95+
Note that this mechanism being based on DNS names, ability to control DNS resolution
96+
would obviously allow circumventing this, granted the corresponding registry(-ies) would
97+
service requests on a different hostname.
98+
99+
### hosts.toml file with a "server" directive
100+
101+
The simplest way to configure a different _registry endpoint_ is to use the `server`
102+
section of the `hosts.toml` file:
103+
104+
Effectively, `~/.config/containerd/certs.d/docker.io:443/hosts.toml`
105+
```toml
106+
server = "https://myserver.example:1234"
107+
```
108+
109+
... will make all requests using _namespace_ `docker.io` talk with `myserver.example:1234`.
110+
111+
Note that, in order:
112+
- if you omit the scheme part of the url, `https` is implied
113+
- if you specify any directive applying to the server that implies TLS communication, the scheme will be forced to `https`
114+
- if you omit the port part of the url:
115+
- port `443` is implied if the scheme is `https`
116+
- port `80` is implied if the scheme is (explicitly) `http`
117+
118+
Note that if you do omit the server directive in your `hosts.toml`, the default, _implied
119+
host_ for that _namespace_ will be used instead. The _implied host_ for a _namespace_ is decided as:
120+
- take the host (and optional port) of the namespace
121+
- if the port is omitted in the _namespace_, default port 443 is used
122+
- scheme `https` is used, enforcing TLS communication
123+
124+
See section about the `--insecure-registry` flag and `localhost` for exceptions.
125+
126+
### hosts.toml with "hosts" segments
127+
128+
You can further control resolution by adding hosts segments:
129+
130+
```toml
131+
server = "https://myserver.example:1234"
132+
133+
[host."http://another-endpoint.example:4567"]
134+
capabilities = ["pull", "resolve", "push"]
135+
```
136+
137+
In that case, nerdctl will first try all hosts segments successively with the following algorithm:
138+
- if the host does not specify any capability, it is assumed that is has all capabilities
139+
- if the host has a capability that matches the requested operation, try it
140+
- if the operation is successful with that host, we are done
141+
- if the operation was unsuccesful, continue to the next host
142+
- if the host does not have the capability to match the requested operation, continue to the next host
143+
144+
Once all configured hosts have been exhausted unsuccessfully, nerdctl will try the `server`
145+
(explicit or implied).
146+
147+
Note that hosts directives use the same heuristic as server with regard to scheme and port.
148+
149+
### Non-compliant hosts
150+
151+
Hosts that do implement the protocol correctly should serve under the `/v2/` root path.
152+
153+
To configure a non-compliant host, you may pass along `override_path = true` as a property,
154+
and specify the full url you expect in the host segment.
155+
156+
### TLS configuration, custom headers, etc...
157+
158+
Both server and hosts segments can specify custom TLS configuration, like a custom CA,
159+
client certificates, and the ability to skip verification of TLS certificates, along
160+
with the ability to pass additional http headers.
161+
162+
TL;DR:
163+
```toml
164+
ca = "/etc/certs/myca.pem"
165+
skip_verify = false
166+
client = [["/etc/certs/client.cert", "/etc/certs/client.key"],["/etc/certs/client.pem", ""]]
167+
[header]
168+
x-custom = "my custom header"
169+
```
170+
171+
Refer to the `hosts.toml` dedicated documentation for details.
172+
173+
## HTTP requests
174+
175+
Requests sent to a configured `server` or `host` will add a query parameter to the urls.
176+
For example:
177+
178+
```bash
179+
http://myserver.example/v2/library/debian/manifests/latest?ns=docker.io
180+
```
181+
182+
This allows registry servers to understand for what namespace they are serving
183+
resources, and possibly perform additional operations.
184+
185+
Obviously, nothing prevents a registry server to be used both as a default server
186+
for a namespace, and also as an endpoint for another.
187+
188+
## What happens with localhost?
189+
190+
If localhost is used as a _registry namespace_ without any specific configuration,
191+
it is by default treated as if the following had been set in its toml file:
192+
193+
`~/.config/containerd/certs.d/localhost:443/hosts.toml`
194+
```toml
195+
server = "http://localhost:80"
196+
197+
[host."https://localhost:443"]
198+
skip_verify = true
199+
```
200+
201+
Specifying a port (`localhost:1234`) will not change the overall behavior.
202+
It will be equivalent to setting the following file:
203+
204+
`~/.config/containerd/certs.d/localhost:1234/hosts.toml`
205+
```toml
206+
[host."https://localhost:1234"]
207+
skip_verify = true
208+
[host."http://localhost:1234"]
209+
```
210+
211+
This behavior is historical (and subject to change by docker as well), and can be disabled
212+
for nerdctl by passing an explicit `--insecure-registry=false`, in which case `localhost` will be treated
213+
as any other namespace.
214+
215+
All of the above solely applies when `localhost` is used as an un-configured namespace.
216+
217+
## What does `nerdctl --insecure-registry` do?
218+
219+
This is a custom flag supported only by nerdctl (docker does not support it).
220+
221+
Using it is discouraged, as its design is inconsistent with the `hosts.toml` mechanism
222+
which should be used instead.
223+
224+
The flag only applies when used against a _registry namespace_ with **no** explicit hosts.toml
225+
configuration.
226+
In that scenario, when `--insecure-registry=true` is specified, it will behave as if the
227+
following hosts.toml had been configured.
228+
229+
For namespace `mynamespace.example` (no port):
230+
231+
`~/.config/containerd/certs.d/mynamespace.example:443/hosts.toml`
232+
```toml
233+
server = "http://mynamespace.example:80"
234+
[host."https://mynamespace.example:443"]
235+
skip_verify = true
236+
```
237+
238+
For namespace `mynamespace.example:1234`:
239+
240+
`~/.config/containerd/certs.d/mynamespace.example:1234/hosts.toml`
241+
```toml
242+
server = "http://mynamespace.example:1234"
243+
[host."https://mynamespace.example:1234"]
244+
skip_verify = true
245+
```
246+
247+
For namespace `mynamespace.example:443`:
248+
249+
`~/.config/containerd/certs.d/mynamespace.example:443/hosts.toml`
250+
```toml
251+
server = "http://mynamespace.example:443"
252+
[host."https://mynamespace.example:443"]
253+
skip_verify = true
254+
```
255+
256+
For namespace `mynamespace.example:80`:
257+
258+
`~/.config/containerd/certs.d/mynamespace.example:80/hosts.toml`
259+
```toml
260+
server = "http://mynamespace.example:80"
261+
[host."https://mynamespace.example:80"]
262+
skip_verify = true
263+
```
264+
265+
The effect of `--insecure-registry=false` is generally a no-op, except in the case of
266+
localhost as described above.
267+
268+
Note that using `--insecure-registry=true` on a namespace that DO have an explicit `hosts.toml`
269+
configuration is a no-op as well.
270+
271+
## Authentication
272+
273+
In its simple form, `nerdctl login` will behave exactly
274+
the same way as docker (which does not support `hosts.toml`).
275+
276+
For example:
277+
```nerdctl login namespace.example```
278+
279+
Will resolve to the implied _registry endpoint_ `https://namespace.example:443/`
280+
and authenticate there either prompting for credentials, or, if they exist,
281+
retrieving credentials from the docker store.
282+
283+
The `--insecure-registry` flag will work in that case with the same semantics as
284+
outlined above.
285+
286+
Now, when `server` and `hosts` configuration are involved, the behavior is different.
287+
288+
If there are `host` directives:
289+
290+
1. and there is no `server` directive, or if the `server` directive matches the scheme, domain and port
291+
of the requested _registry namespace_ implied server, `nerdctl login` will function as above,
292+
but will additionally notify the user that additional endpoints exist for that namespace,
293+
and instruct the user to log in to these endpoints additionally if they need to.
294+
2. if on the other hand there is a `server` directive that does NOT match the namespace host,
295+
`nerdctl login` will decline to log in, and instruct the user to use the endpoint login syntax instead
296+
297+
To log in into a specific _endpoint_ for a _registry namespace_, you should use the
298+
additional login flag `--endpoint`.
299+
300+
For example:
301+
```bash
302+
nerdctl login namespace.example --endpoint myserver.example
303+
```
304+
305+
Will proceed with the following steps:
306+
- check that there is indeed a `myserver.example` endpoint configured in the hosts.toml for `namespace.example`
307+
- if there is one, try to authenticate against `https://myserver.example:443/v2/?ns=https://namespace.example:443`
308+
309+
Note that:
310+
- implied scheme and port resolution follow the same rules outlined above,
311+
both for the namespace and the endpoint
312+
- the flag `--insecure-registry` is a no-op
313+
314+
## Credentials storage
315+
316+
As outlined, credentials are stored using docker facilities.
317+
318+
This is usually stored inside the file `$DOCKER_CONFIG/config.json`,
319+
and credentials are keyed per-namespace host (domain+port), except for
320+
the docker hub registry which uses a fully qualified URL.
321+
322+
Since docker does not support `hosts.toml` and since _endpoints_ are not
323+
the same thing as an implied registry host for a namespace, we store
324+
_endpoint_ credentials using a different schema.
325+
326+
Docker will not recognize this schema, hence will not wrongly send these
327+
credentials when trying to log in into a known _endpoint_ as a registry.
328+
329+
The schema is: `nerdctl-experimental://namespace.example:123/?endpoint=myserver.example:456`
330+
331+
As clearly shown above, this is currently experimental, and is subject to change
332+
in the future.
333+
There is no guarantees that credentials stored that way will be able to be retrieved
334+
by future nerdctl versions.

0 commit comments

Comments
 (0)