Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
[package]
name = "rustfs-operator"
version = "0.6.4"
version = "0.7.0"
edition = "2024"
rust-version = "1.92"
license = "MIT OR Apache-2.0"
description = "Kubernetes operator managing RustFS buckets, IAM users and policies"

[dependencies]
# RustFS management-plane client
rc-core = "0.1.31"
rc-s3 = "0.1.31"
rc-core = "0.1.32"
rc-s3 = "0.1.32"

# Kubernetes
kube = { version = "4.0", default-features = false, features = ["client", "runtime", "derive", "rustls-tls", "ring"] }
Expand Down
12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,14 @@ User "spark" (policies attach here)
The operator writes each generated `accessKey`/`secretKey`/`endpoint` into a
Secret in the CR's namespace, owner-referenced so it is garbage-collected
with the CR; if that Secret is lost the key is revoked and reissued. Keys
are issued while authenticated *as the owning user*, so an AccessKey needs
the user's password and the user's policies must allow
`admin:CreateServiceAccount`, `admin:ListServiceAccounts` and
`admin:RemoveServiceAccount`.
are issued by the operator's admin credential, which names the owner with
`targetUser`; the server accepts that only from an owner credential, so the
connection must hold RustFS root.

**See [docs/iam-model.md](docs/iam-model.md)** for how parenting works, why
the password is required (a client-library gap, not a server one), and how
to inspect identities with `rc`.
root is required, and how to inspect identities with `rc`. Upgrading from
0.6.x is a breaking change — `AccessKey.passwordRef` / the chart's
`passwordFromUser` are gone.

Namespaced resources select a RustFS server via `spec.connection`, in one of
two mutually exclusive ways:
Expand Down
4 changes: 2 additions & 2 deletions charts/rustfs-operator-crds/Chart.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,5 @@ keywords:
- operator
- crds
# version and appVersion are overwritten from the git tag by the release workflow
version: 0.6.4
appVersion: "0.6.4"
version: 0.7.0
appVersion: "0.7.0"
13 changes: 10 additions & 3 deletions charts/rustfs-operator-crds/templates/accesskeys.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,12 @@ spec:
nullable: true
type: string
passwordRef:
description: Secret holding that user's password (key defaults to `password`).
description: |-
REMOVED in 0.7.0 and rejected if set. Keys are now issued by the admin
credential via `targetUser`, so the owning user's password is no longer
needed. Retained in the schema only so a stale manifest fails loudly
instead of being silently pruned; drop the field.
nullable: true
properties:
key:
description: Key within the Secret; each consumer documents its default.
Expand All @@ -112,11 +117,13 @@ spec:
nullable: true
type: string
user:
description: Username of the owning RustFS user.
description: |-
Username of the owning RustFS user. The key is parented to this user
via `targetUser`, which the server honours only for an owner
credential — the connection must hold RustFS root.
type: string
required:
- connection
- passwordRef
- user
type: object
status:
Expand Down
4 changes: 2 additions & 2 deletions charts/rustfs-operator/Chart.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,5 @@ keywords:
- s3
- operator
# version and appVersion are overwritten from the git tag by the release workflow
version: 0.6.4
appVersion: "0.6.4"
version: 0.7.0
appVersion: "0.7.0"
13 changes: 10 additions & 3 deletions charts/rustfs-operator/crds/crds.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -376,7 +376,12 @@ spec:
nullable: true
type: string
passwordRef:
description: Secret holding that user's password (key defaults to `password`).
description: |-
REMOVED in 0.7.0 and rejected if set. Keys are now issued by the admin
credential via `targetUser`, so the owning user's password is no longer
needed. Retained in the schema only so a stale manifest fails loudly
instead of being silently pruned; drop the field.
nullable: true
properties:
key:
description: Key within the Secret; each consumer documents its default.
Expand All @@ -402,11 +407,13 @@ spec:
nullable: true
type: string
user:
description: Username of the owning RustFS user.
description: |-
Username of the owning RustFS user. The key is parented to this user
via `targetUser`, which the server honours only for an owner
credential — the connection must hold RustFS root.
type: string
required:
- connection
- passwordRef
- user
type: object
status:
Expand Down
4 changes: 2 additions & 2 deletions charts/rustfs-resources/Chart.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,5 @@ keywords:
- s3
- operator
# version and appVersion are overwritten from the git tag by the release workflow
version: 0.6.4
appVersion: "0.6.4"
version: 0.7.0
appVersion: "0.7.0"
19 changes: 9 additions & 10 deletions charts/rustfs-resources/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,15 +36,13 @@ policies:

users:
- name: app-user
policies: ["app-data-rw"] # must include admin:*ServiceAccount actions
policies: ["app-data-rw"]
passwordRef: # existing Secret with key `password`
name: app-user-creds

accessKeys:
- name: app-key # operator writes AK/SK to Secret
user: app-user # "app-key-credentials" in this namespace
passwordRef:
name: app-user-creds
```

## Values
Expand All @@ -55,7 +53,7 @@ accessKeys:
| `buckets[]` | `name` (required), `bucketName`, `versioning`, `quotaBytes`, `deletionPolicy`, `connection` |
| `policies[]` | `name` (required), `document` (required), `policyName`, `deletionPolicy`, `connection` |
| `users[]` | `name` (required), `username`, `passwordRef` **or** inline `password`, `policies`, `enabled`, `deletionPolicy`, `connection` |
| `accessKeys[]` | `name`, `user` (required), `passwordRef` **or** `passwordFromUser`, `accessKey`, `description`, `policy`, `targetSecretName`, `deletionPolicy`, `connection` |
| `accessKeys[]` | `name`, `user` (required), `accessKey`, `description`, `policy`, `targetSecretName`, `deletionPolicy`, `connection` |

Fields you omit stay unmanaged (e.g. no `versioning` key means the operator
never touches versioning). `deletionPolicy` defaults to `Delete` — the
Expand All @@ -75,12 +73,13 @@ entry is only needed for per-consumer or reduced-scope credentials
`<name>-credentials`). The key inherits that user's policies, so grant
access via `users[].policies` rather than per key.

The password is needed because the operator authenticates as the user to
issue its keys: set `passwordRef`, or `passwordFromUser: <users[] entry>` to
reuse the password Secret this chart created for that user. For the same
reason the user's policies must allow `admin:CreateServiceAccount`,
`admin:ListServiceAccounts` and `admin:RemoveServiceAccount` over itself.
See [docs/iam-model.md](../../docs/iam-model.md).
Only `user` is needed: since chart 0.7.0 the operator issues keys with its own
admin credential, naming the owner via `targetUser`. `passwordFromUser` and
`passwordRef` were removed and are rejected at render time, and the owning
user no longer needs `admin:CreateServiceAccount` /
`admin:ListServiceAccounts` / `admin:RemoveServiceAccount`. This requires the
operator's connection to hold RustFS root — see
[docs/iam-model.md](../../docs/iam-model.md).

## Prerequisites

Expand Down
20 changes: 2 additions & 18 deletions charts/rustfs-resources/templates/accesskey.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,8 @@
{{- if eq $user "" }}
{{- fail (printf "%s: 'user' (the owning RustFS username) is required" $id) }}
{{- end }}
{{- $fromUser := .passwordFromUser | default "" }}
{{- $ref := .passwordRef | default dict }}
{{- $refName := $ref.name | default "" }}
{{- if and (ne $fromUser "") (ne $refName "") }}
{{- fail (printf "%s: set either passwordRef or passwordFromUser, not both" $id) }}
{{- end }}
{{- if and (eq $fromUser "") (eq $refName "") }}
{{- fail (printf "%s: a password source is required; set passwordRef or passwordFromUser (a users[] entry with inline password)" $id) }}
{{- if or (hasKey . "passwordFromUser") (hasKey . "passwordRef") }}
{{- fail (printf "%s: passwordFromUser/passwordRef were removed in chart 0.7.0. The operator now issues keys with its own admin credential using targetUser, so the owning user's password is no longer needed. Drop the field, and drop the admin:CreateServiceAccount / ListServiceAccounts / RemoveServiceAccount grants from that user's policy if nothing else needs them" $id) }}
{{- end }}
{{- $ak := .accessKey | default "" }}
{{- if and (ne $ak "") (eq $ak $user) }}
Expand All @@ -29,16 +23,6 @@ metadata:
spec:
{{- include "rustfs-resources.connection" (dict "root" $ "entry" . "id" $id) | nindent 2 }}
user: {{ $user | quote }}
passwordRef:
{{- if ne $fromUser "" }}
# the Secret this chart creates for the referenced users[] entry
name: {{ printf "%s-user-%s" $.Release.Name $fromUser }}
{{- else }}
name: {{ $refName }}
{{- with $ref.key }}
key: {{ . }}
{{- end }}
{{- end }}
{{- with .accessKey }}
accessKey: {{ . | quote }}
{{- end }}
Expand Down
8 changes: 2 additions & 6 deletions charts/rustfs-resources/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,6 @@ users: []
# # ... OR let the chart create one (WARNING: the password then lives
# # in the Helm release values; prefer passwordRef in production):
# password: ""
# # to issue access keys for this user, its policies must include the
# # admin:CreateServiceAccount / ListServiceAccounts / RemoveServiceAccount actions
# policies: ["app-data-rw"]
# enabled: true # optional
# deletionPolicy: Delete
Expand All @@ -50,10 +48,8 @@ accessKeys: []
# accessKeys:
# - name: app-key # AK/SK pair for a user; credentials are
# user: app-user # written by the operator to a Secret
# # password source for the owning user, EITHER:
# passwordFromUser: app-user # a users[] entry above with inline password
# # OR an explicit Secret reference:
# # passwordRef: {name: app-user-creds, key: password}
# # No password source: the operator issues the key with its own admin
# # credential via targetUser, which requires that credential to be root.
# accessKey: "" # optional; generated when empty
# description: ""
# policy: {} # optional inline policy restricting the key
Expand Down
13 changes: 10 additions & 3 deletions deploy/crds.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -376,7 +376,12 @@ spec:
nullable: true
type: string
passwordRef:
description: Secret holding that user's password (key defaults to `password`).
description: |-
REMOVED in 0.7.0 and rejected if set. Keys are now issued by the admin
credential via `targetUser`, so the owning user's password is no longer
needed. Retained in the schema only so a stale manifest fails loudly
instead of being silently pruned; drop the field.
nullable: true
properties:
key:
description: Key within the Secret; each consumer documents its default.
Expand All @@ -402,11 +407,13 @@ spec:
nullable: true
type: string
user:
description: Username of the owning RustFS user.
description: |-
Username of the owning RustFS user. The key is parented to this user
via `targetUser`, which the server honours only for an owner
credential — the connection must hold RustFS root.
type: string
required:
- connection
- passwordRef
- user
type: object
status:
Expand Down
11 changes: 4 additions & 7 deletions deploy/example.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -85,12 +85,11 @@ spec:
enabled: true
---
# An AK/SK credential pair owned by app-user, inheriting its policies. The
# operator issues it in RustFS (authenticating as the user, hence the
# password) and writes accessKey/secretKey/endpoint into the Secret
# operator issues it with its own admin credential, naming the owner via
# targetUser, and writes accessKey/secretKey/endpoint into the Secret
# "app-key-credentials" (owner-referenced, GC'd with the CR).
# NOTE: the user's policies must include admin:CreateServiceAccount,
# admin:ListServiceAccounts and admin:RemoveServiceAccount.
# See docs/iam-model.md for the full model.
# NOTE: the connection must hold RustFS root — the server only lets an owner
# parent a key to another user. See docs/iam-model.md for the full model.
apiVersion: rustfs.com/v1alpha1
kind: AccessKey
metadata:
Expand All @@ -99,5 +98,3 @@ spec:
connection:
secretRef: rustfs-conn
user: app-user
passwordRef:
name: app-user-creds
51 changes: 37 additions & 14 deletions docs/iam-model.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,19 +112,38 @@ Rule of thumb: one workload per identity is fine on the user credential;
several workloads, or anything needing per-consumer revocation or reduced
scope, wants access keys.

## Why `AccessKey` needs the user's password
## How `AccessKey` issues keys

The operator authenticates **as the user** to issue that user's keys, which
is why `AccessKey` requires `passwordRef` (or `passwordFromUser`) and why
the owning user's policies must allow `admin:CreateServiceAccount`,
`admin:ListServiceAccounts` and `admin:RemoveServiceAccount` over itself.
The operator issues keys with its **admin credential**, naming the owner via
`targetUser`. The `AccessKey` CR therefore needs nothing but the username:

This is a limitation of the client library, not of RustFS. The server
accepts `targetUser`, so the admin credential alone would be enough — but
`targetUser` is not exposed by `rc-core`/`rc-s3`, so the operator cannot
send it. Tracked upstream in
[rustfs/cli#340](https://github.com/rustfs/cli/issues/340); once it lands,
the password requirement and the per-user admin actions can both be dropped.
```yaml
spec:
connection: { clusterRef: default }
user: spark # becomes targetUser
```

**The connection must hold RustFS root.** The server's guard is
`owner || target_user == req_user || target_user == req_parent_user`, so only
an owner credential may parent a key to somebody else; a scoped admin gets
`service account parent is outside requester scope`. This is deliberate —
`admin:CreateServiceAccount` controls *whether* a caller can create keys, not
*for whom*, and without the guard any holder of that action could mint a
root-parented key and escalate (GHSA-5354).

Before 0.7.0 the operator authenticated *as the owning user*, which meant every
`AccessKey` carried `passwordRef`/`passwordFromUser` and every owning user
needed `admin:CreateServiceAccount`, `admin:ListServiceAccounts` and
`admin:RemoveServiceAccount` over itself. That was a client-library gap
([rustfs/cli#340](https://github.com/rustfs/cli/issues/340)), not a server one;
it was fixed by [#352](https://github.com/rustfs/cli/pull/352) and shipped in
rc-core 0.1.32, which the operator now requires.

Migrating from 0.6.x: delete `passwordRef` / `passwordFromUser` from every
`AccessKey`, and drop the three `admin:*ServiceAccount` grants from the owning
user's policy unless something else needs them. A leftover `passwordRef` is
rejected as a spec error rather than ignored, and the chart fails at render
time, so a stale manifest cannot silently keep working.

## Reading the server directly

Expand All @@ -139,9 +158,13 @@ rc admin service-account list <alias> --user spark # that user's keys
rc admin service-account list <alias> # the alias's OWN keys
```

The `parent:` shown in the listing is the owning user. A key created with
`rc admin service-account create` is always parented to the alias's
identity, since the CLI cannot send `targetUser` either.
The `parent:` shown in the listing is the owning user. Since rc 0.1.32,
`create` also takes `--user` to parent a new key to someone else (owner
credentials only); without it the key is parented to the alias's own identity:

```sh
rc admin service-account create <alias> <AK> <SK> --user spark # rc >= 0.1.32
```

## Other server behaviours worth knowing

Expand Down
Loading
Loading