Skip to content

Commit c40cf68

Browse files
committed
feat!: correct IAM model — User is username/password, AccessKeys are per-user credentials
BREAKING: UserSpec.accessKey/secretKeyRef renamed to username/passwordRef (password Secret key defaults to 'password'). Existing User CRs must be updated. New AccessKey CRD: issues an AK/SK service account for a User. RustFS only mints service accounts for the calling identity, so the operator authenticates as the user (password from passwordRef); generated credentials are written to a Secret (default <name>-credentials) with an ownerReference for GC. Lost credential Secrets cause revoke + reissue. The user's policies must allow admin:CreateServiceAccount / ListServiceAccounts / RemoveServiceAccount (verified against the beta server, which also enforces AK<=20 / SK<=40 chars and rejects deleting attached policies). Charts: rustfs-resources gains accessKeys[] (passwordFromUser convenience) and renames user fields; operator chart RBAC gains secret write verbs and rbac.secretNamespaces allowlist for least-privilege cross-namespace setups; crds chart ships the new CRD.
1 parent 4bd3cf5 commit c40cf68

32 files changed

Lines changed: 1508 additions & 187 deletions

Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "rustfs-operator"
3-
version = "0.5.0"
3+
version = "0.6.0"
44
edition = "2024"
55
rust-version = "1.92"
66
license = "MIT OR Apache-2.0"
@@ -35,6 +35,7 @@ anyhow = "1"
3535
tracing = "0.1"
3636
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
3737
clap = { version = "4", features = ["derive", "env"] }
38+
rand = "0.9"
3839

3940
[dev-dependencies]
4041
mockall = "0.14"

README.md

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,23 @@ the [`rc-core`](https://crates.io/crates/rc-core) /
88

99
## CRDs (`rustfs.com/v1alpha1`)
1010

11-
| Kind | Short name | Scope | Manages |
12-
|---------------------|------------|------------|-----------------------------------------------|
13-
| `Bucket` | `rfb` | namespaced | bucket existence, versioning, hard quota |
14-
| `User` | `rfu` | namespaced | IAM user, enabled/disabled, attached policies |
15-
| `Policy` | `rfp` | namespaced | IAM policy document (inline YAML/JSON) |
16-
| `ClusterConnection` | `rfcc` | cluster | centrally managed RustFS server connection |
11+
| Kind | Short name | Scope | Manages |
12+
|---------------------|------------|------------|--------------------------------------------------------|
13+
| `Bucket` | `rfb` | namespaced | bucket existence, versioning, hard quota |
14+
| `User` | `rfu` | namespaced | IAM identity (username + password), attached policies |
15+
| `AccessKey` | `rfak` | namespaced | AK/SK credential pair for a User, written to a Secret |
16+
| `Policy` | `rfp` | namespaced | IAM policy document (inline YAML/JSON) |
17+
| `ClusterConnection` | `rfcc` | cluster | centrally managed RustFS server connection |
18+
19+
The IAM model mirrors RustFS: a **User** is an identity (username/password)
20+
that policies attach to; applications authenticate with **AccessKeys**
21+
(a user can have many). The operator issues each AccessKey while
22+
authenticated *as the user* and writes the generated `accessKey`/
23+
`secretKey`/`endpoint` into a Secret in the CR's namespace, owner-referenced
24+
so it is garbage-collected with the CR. If that Secret is lost, the key is
25+
revoked and reissued. For a user to manage its own keys, its policies must
26+
allow `admin:CreateServiceAccount`, `admin:ListServiceAccounts` and
27+
`admin:RemoveServiceAccount`.
1728

1829
Namespaced resources select a RustFS server via `spec.connection`, in one of
1930
two mutually exclusive ways:
@@ -89,8 +100,11 @@ build and attach a linux-amd64 binary.
89100

90101
- **Reconcile loop**: finalizer-based; drift is re-checked every 5 minutes,
91102
errors retry after 15s and are reported in `.status.message`.
92-
- **User secret keys** are only applied at user creation; RustFS does not
93-
expose secret keys, so rotating one requires deleting/recreating the user.
103+
- **User passwords** are only applied at user creation; RustFS cannot
104+
update them in place. Rotate credentials by rotating AccessKeys instead
105+
(delete/recreate the AccessKey CR).
106+
- **Policies in use cannot be deleted**: RustFS rejects deleting a policy
107+
that is still attached; the Policy CR reports the error and retries.
94108
- **Policy attachment** uses RustFS's `set-user-or-group-policy` endpoint,
95109
which *replaces* the whole attachment set — `spec.policies` is therefore
96110
fully declarative.

charts/rustfs-operator-crds/Chart.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,5 @@ keywords:
1111
- operator
1212
- crds
1313
# version and appVersion are overwritten from the git tag by the release workflow
14-
version: 0.5.0
15-
appVersion: "0.5.0"
14+
version: 0.6.0
15+
appVersion: "0.6.0"
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
{{- if .Values.crds.accessKey }}
2+
apiVersion: apiextensions.k8s.io/v1
3+
kind: CustomResourceDefinition
4+
metadata:
5+
name: accesskeys.rustfs.com
6+
{{- if or .Values.keep .Values.annotations }}
7+
annotations:
8+
{{- if .Values.keep }}
9+
helm.sh/resource-policy: keep
10+
{{- end }}
11+
{{- with .Values.annotations }}
12+
{{- toYaml . | nindent 4 }}
13+
{{- end }}
14+
{{- end }}
15+
labels:
16+
app.kubernetes.io/name: rustfs-operator-crds
17+
app.kubernetes.io/managed-by: {{ .Release.Service }}
18+
helm.sh/chart: {{ printf "%s-%s" .Chart.Name .Chart.Version }}
19+
{{- with .Values.labels }}
20+
{{- toYaml . | nindent 4 }}
21+
{{- end }}
22+
spec:
23+
group: rustfs.com
24+
names:
25+
categories: []
26+
kind: AccessKey
27+
plural: accesskeys
28+
shortNames:
29+
- rfak
30+
singular: accesskey
31+
scope: Namespaced
32+
versions:
33+
- additionalPrinterColumns:
34+
- jsonPath: .status.ready
35+
name: Ready
36+
type: boolean
37+
- jsonPath: .status.accessKey
38+
name: AccessKey
39+
type: string
40+
- jsonPath: .status.message
41+
name: Message
42+
type: string
43+
name: v1alpha1
44+
schema:
45+
openAPIV3Schema:
46+
description: Auto-generated derived type for AccessKeySpec via `CustomResource`
47+
properties:
48+
spec:
49+
description: |-
50+
An access key (AK/SK credential pair, a RustFS "service account") owned
51+
by a [`User`]. A user can have many. The operator authenticates to
52+
RustFS *as the user* to issue the key (the admin API only mints keys for
53+
the calling identity), generates the credentials, and writes them to a
54+
Secret in the CR's namespace, owner-referenced so it is garbage-collected
55+
with the CR.
56+
properties:
57+
accessKey:
58+
description: Explicit access key id; generated when omitted.
59+
nullable: true
60+
type: string
61+
connection:
62+
description: |-
63+
Reference to a RustFS server. Exactly one of `secretRef` / `clusterRef`
64+
must be set (validated at reconcile time).
65+
properties:
66+
clusterRef:
67+
description: Name of a cluster-scoped ClusterConnection.
68+
nullable: true
69+
type: string
70+
secretRef:
71+
description: |-
72+
Name of a Secret in the resource's namespace holding `endpoint`,
73+
`accessKey` and `secretKey` (optional: `region`, `insecure`).
74+
nullable: true
75+
type: string
76+
type: object
77+
deletionPolicy:
78+
default: Delete
79+
description: What happens to the remote resource when the CR is deleted.
80+
enum:
81+
- Delete
82+
- Retain
83+
type: string
84+
description:
85+
description: Optional description stored on the service account.
86+
nullable: true
87+
type: string
88+
passwordRef:
89+
description: Secret holding that user's password (key defaults to `password`).
90+
properties:
91+
key:
92+
description: Key within the Secret; each consumer documents its default.
93+
nullable: true
94+
type: string
95+
name:
96+
description: Secret name.
97+
type: string
98+
required:
99+
- name
100+
type: object
101+
policy:
102+
description: |-
103+
Optional inline policy restricting what this key may do (subset of
104+
the user's permissions), written as YAML/JSON.
105+
type: object
106+
x-kubernetes-preserve-unknown-fields: true
107+
targetSecretName:
108+
description: |-
109+
Name of the Secret the operator writes the credentials to
110+
(keys `accessKey`, `secretKey`, `endpoint`); defaults to
111+
`<cr-name>-credentials`.
112+
nullable: true
113+
type: string
114+
user:
115+
description: Username of the owning RustFS user.
116+
type: string
117+
required:
118+
- connection
119+
- passwordRef
120+
- user
121+
type: object
122+
status:
123+
description: Status for AccessKey resources.
124+
nullable: true
125+
properties:
126+
accessKey:
127+
description: The issued access key id.
128+
nullable: true
129+
type: string
130+
message:
131+
nullable: true
132+
type: string
133+
observedGeneration:
134+
format: int64
135+
nullable: true
136+
type: integer
137+
ready:
138+
default: false
139+
type: boolean
140+
type: object
141+
required:
142+
- spec
143+
title: AccessKey
144+
type: object
145+
served: true
146+
storage: true
147+
subresources:
148+
status: {}
149+
{{- end }}

charts/rustfs-operator-crds/templates/users.yaml

Lines changed: 22 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -43,12 +43,11 @@ spec:
4343
description: Auto-generated derived type for UserSpec via `CustomResource`
4444
properties:
4545
spec:
46-
description: An IAM user in RustFS.
46+
description: |-
47+
An IAM user (identity) in RustFS: a username with a password. Policies
48+
attach to the user; applications should authenticate with [`AccessKey`]s
49+
issued for the user rather than the password itself.
4750
properties:
48-
accessKey:
49-
description: Access key (username) in RustFS; defaults to the CR name.
50-
nullable: true
51-
type: string
5251
connection:
5352
description: |-
5453
Reference to a RustFS server. Exactly one of `secretRef` / `clusterRef`
@@ -76,30 +75,36 @@ spec:
7675
default: true
7776
description: Whether the user is enabled.
7877
type: boolean
79-
policies:
80-
default: []
78+
passwordRef:
8179
description: |-
82-
Policies attached to the user; managed declaratively (extra
83-
attachments are detached).
84-
items:
85-
type: string
86-
type: array
87-
secretKeyRef:
88-
description: Secret holding the user's secret key (used at creation time).
80+
Secret holding the user's password (key defaults to `password`).
81+
Only applied when the user is first created; RustFS cannot update it.
8982
properties:
9083
key:
91-
default: secretKey
92-
description: Key within the Secret; defaults to `secretKey`.
84+
description: Key within the Secret; each consumer documents its default.
85+
nullable: true
9386
type: string
9487
name:
9588
description: Secret name.
9689
type: string
9790
required:
9891
- name
9992
type: object
93+
policies:
94+
default: []
95+
description: |-
96+
Policies attached to the user; managed declaratively (extra
97+
attachments are detached).
98+
items:
99+
type: string
100+
type: array
101+
username:
102+
description: Username in RustFS; defaults to the CR name.
103+
nullable: true
104+
type: string
100105
required:
101106
- connection
102-
- secretKeyRef
107+
- passwordRef
103108
type: object
104109
status:
105110
description: Shared status for all RustFS resources.

charts/rustfs-operator-crds/values.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,5 @@ crds:
1313
bucket: true
1414
user: true
1515
policy: true
16+
accessKey: true
1617
clusterConnection: true

charts/rustfs-operator/Chart.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,5 @@ keywords:
1010
- s3
1111
- operator
1212
# version and appVersion are overwritten from the git tag by the release workflow
13-
version: 0.5.0
14-
appVersion: "0.5.0"
13+
version: 0.6.0
14+
appVersion: "0.6.0"

charts/rustfs-operator/README.md

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ helm install rustfs-operator rustfs-operator/rustfs-operator \
2222
| `serviceAccount.create` | `true` | create the ServiceAccount |
2323
| `rbac.create` | `true` | create ClusterRole/Role and bindings |
2424
| `rbac.clusterWideSecrets` | `true` | see [RBAC](#rbac) |
25+
| `rbac.secretNamespaces` | `[]` | namespaces granted Secret access via Roles when `clusterWideSecrets` is off |
2526
| `clusterConnections` | `[]` | bootstrap ClusterConnection resources, see below |
2627

2728
## Bootstrapping ClusterConnections
@@ -61,9 +62,9 @@ clusterConnections:
6162

6263
## RBAC
6364

64-
`rbac.clusterWideSecrets: true` (default) lets the operator read Secrets in
65-
all namespaces, which `connection.secretRef` and per-namespace
66-
`User.spec.secretKeyRef` need. When you use **only** `ClusterConnection`
67-
(and keep `User` resources in the operator's namespace), set it to `false`
68-
for least privilege — the operator then reads Secrets only in its own
69-
namespace via a namespaced Role.
65+
`rbac.clusterWideSecrets: true` (default) lets the operator read and write
66+
Secrets in all namespaces (read: `connection.secretRef` and
67+
`User`/`AccessKey` `passwordRef`; write: `AccessKey` credential Secrets).
68+
For least privilege set it to `false` and list the application namespaces
69+
that hold RustFS resources in `rbac.secretNamespaces` — the operator then
70+
gets a namespaced Role in exactly those namespaces (plus its own).

0 commit comments

Comments
 (0)