Gate local repo writes behind OIDC tokens (#169)
Local repo writes (file uploads/deletes, object evictions, native publishes, docker push) are open to anyone who can reach the service; only remote/virtual config is token-gated. - add `ARTIFACTAPI_AUTH_UPLOADS` (default false) gating every local write path behind the OIDC verifier - accept the token as Bearer, Token, Basic password (any user) or bare header value on every gated path, including remote/virtual config writes - serve a `/v2/token` Bearer challenge flow so `docker login` with the token enables push; missing, expired or invalid credentials get an anonymous token so pulls stay open - document minting a Vault identity token and passing it per client Reviewed-on: #169 Co-authored-by: unkin-agent <unkin-agent@unkin.net> Co-committed-by: unkin-agent <unkin-agent@unkin.net>
This commit was merged in pull request #169.
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
|
||||
A local `ansible` repo is a Galaxy v3 server: `ansible-galaxy collection publish`
|
||||
and `ansible-galaxy collection install` work against `/api/v1/local/<repo>`.
|
||||
`publish` needs an API key; any value works.
|
||||
`publish` needs an API key; any value works unless [uploads are gated](../common/auth.md).
|
||||
|
||||
```bash
|
||||
ansible-galaxy collection build
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
# Write authentication
|
||||
|
||||
artifactapi verifies writes against an OIDC issuer's JWKS, such as a Vault
|
||||
identity OIDC role. Reads, probes and anonymous pulls never need a token.
|
||||
|
||||
| Variable | Gates |
|
||||
|---|---|
|
||||
| `ARTIFACTAPI_OIDC_ISSUER` + `ARTIFACTAPI_OIDC_AUDIENCE` | creating, updating and deleting remotes and virtuals |
|
||||
| `ARTIFACTAPI_AUTH_UPLOADS=true` | also every local repo write: `PUT`/`DELETE /api/v2/remotes/<name>/files/*`, `DELETE /api/v2/{remotes,locals}/<name>/objects/*`, `PUT`/`POST`/`DELETE /api/v1/local/<repo>/*` (npm publish, gem push/yank, ansible-galaxy publish) and Docker Registry V2 push/delete under `/v2` |
|
||||
|
||||
Any token from the issuer whose `aud` is in `ARTIFACTAPI_OIDC_AUDIENCE` may
|
||||
write to every repo.
|
||||
|
||||
## Minting a token
|
||||
|
||||
```sh
|
||||
vault read -field=token identity/oidc/token/<role> > token
|
||||
```
|
||||
|
||||
The role's `client_id` must be one of the configured audiences.
|
||||
|
||||
## Passing the token
|
||||
|
||||
The token is accepted as `Authorization: Bearer <token>`, as `Token <token>`,
|
||||
as the password of HTTP Basic credentials with any username, or as a bare
|
||||
header value.
|
||||
|
||||
```sh
|
||||
# curl
|
||||
curl -fsSL -H "Authorization: Bearer $(cat token)" --upload-file f.tgz \
|
||||
https://artifactapi.example.com/api/v2/remotes/<repo>/files/f.tgz
|
||||
curl -fsSL -u "ci:$(cat token)" --upload-file f.tgz \
|
||||
https://artifactapi.example.com/api/v2/remotes/<repo>/files/f.tgz
|
||||
|
||||
# docker / podman / helm (OCI): any username, token as password
|
||||
docker login artifactapi.example.com -u ci --password-stdin < token
|
||||
helm registry login artifactapi.example.com -u ci --password-stdin < token
|
||||
|
||||
# npm (.npmrc)
|
||||
//artifactapi.example.com/api/v1/local/<repo>/:_authToken=<token>
|
||||
|
||||
# gem
|
||||
GEM_HOST_API_KEY="$(cat token)" gem push --host https://artifactapi.example.com/api/v1/local/<repo> mygem-1.0.0.gem
|
||||
|
||||
# ansible-galaxy
|
||||
ansible-galaxy collection publish c.tar.gz \
|
||||
--server https://artifactapi.example.com/api/v1/local/<repo>/ --api-key "$(cat token)"
|
||||
```
|
||||
|
||||
With uploads gated, `/v2/` answers anonymous requests with a Bearer challenge
|
||||
pointing at `/v2/token`. That endpoint returns the verified Basic password as
|
||||
the registry token, or an anonymous token that only reads, so `docker pull`
|
||||
keeps working without a login.
|
||||
@@ -10,6 +10,9 @@ docker push artifactapi.example.com/docker-internal/myapp:latest
|
||||
docker pull artifactapi.example.com/docker-internal/myapp:latest
|
||||
```
|
||||
|
||||
With [uploads gated](../common/auth.md), `docker login` with any username and
|
||||
a token as the password before pushing; pulls stay anonymous.
|
||||
|
||||
The first path segment after `/v2/` is the artifactapi repo name; the remainder
|
||||
is the image name. Blobs and manifests are stored through the shared
|
||||
content-addressable store (deduplicated by digest, reaped by GC once
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
|
||||
A local `gems` repo is a RubyGems source: `gem push`, `gem yank`, `gem install`
|
||||
and `bundle install` work against `/api/v1/local/<repo>`. `gem push` needs an
|
||||
API key; any value works.
|
||||
API key; any value works unless [uploads are gated](../common/auth.md).
|
||||
|
||||
```bash
|
||||
GEM_HOST_API_KEY=unused gem push --host https://artifactapi.example.com/api/v1/local/<repo> mygem-1.0.0.gem
|
||||
|
||||
+2
-1
@@ -2,7 +2,8 @@
|
||||
|
||||
A local `npm` repo is a real npm registry: `npm publish` (scoped and unscoped)
|
||||
and `npm install` work against `/api/v1/local/<repo>/`. npm requires an
|
||||
`_authToken` for the registry before it will publish; any value works.
|
||||
`_authToken` for the registry before it will publish; any value works unless
|
||||
[uploads are gated](../common/auth.md).
|
||||
|
||||
```ini
|
||||
# .npmrc
|
||||
|
||||
Reference in New Issue
Block a user