This explains how RustFS identities actually work, why the operator's
User and AccessKey CRDs are shaped the way they are, and why an
AccessKey needs the owning user's password. It is written against
RustFS 1.0.0-beta.8.
A user is an identity: a username and a password. It is what policies
attach to. The credential pair is the identity — the username doubles as
an access key and the password as its secret key, which is why
rc admin user info prints Access Key: <username>. You can sign S3
requests with them directly.
An access key (RustFS calls it a service account) is an additional AK/SK pair derived from a user. One user can own many of them.
User "spark" (username + password, policies attached here)
├── AccessKey AKIA...1 → Secret spark-etl-credentials
└── AccessKey AKIA...2 → Secret spark-adhoc-credentials
The operator mirrors this exactly: User manages the identity and its
policy attachments, AccessKey manages one credential pair belonging to a
user and writes it into a Kubernetes Secret.
There is no such thing as a standalone AK/SK in RustFS. Every access key is parented to some user, and that parent determines what the key is allowed to do:
- No policy at creation (the operator's default): the key inherits the
parent user's policies, evaluated at request time. Changing the user's
policies immediately changes what every one of its keys can do. The
server reports these keys with
impliedPolicy: true. - Policy at creation (
spec.policyonAccessKey): the policy is embedded in the key, and a request must be allowed by both the parent's policies and the embedded one. This can only narrow access, never widen it.
So to grant an application access you attach policies to the User, not to
the AccessKey.
The server's add-service-account API decides the parent like this:
| Caller | targetUser |
Resulting parent |
|---|---|---|
| Any user | omitted | the caller itself |
| A service account | omitted | the caller's parent user, not the calling key |
| Root / owner | any user | the named user |
| Non-owner | itself or its own parent | that user (allowed) |
| Non-owner | any other user | rejected — service account parent is outside requester scope |
The guard is literally owner || target_user == req_user || target_user == req_parent_user, so a non-owner is confined to its own scope.
Two consequences are easy to trip over:
Access keys do not nest. A key created while authenticated as another key becomes a sibling of that key, not a child of it. The hierarchy is always exactly two levels deep: user → access keys.
Only the root credential may parent a key to someone else. The
admin:CreateServiceAccount permission controls whether a caller can
create keys, not for whom. This is deliberate — otherwise any holder of
that action could mint a root-parented key and escalate to full ownership.
Because the username is already an access key id, naming a service account after its owner collides. The server resolves the id to the user, fails to load it as a service account, and returns a 500 instead of a clean not-found:
$ rc admin service-account info $USER_ALIAS spark # spark is also the username
✗ HTTP 500: <Code>InternalError</Code><Message>get service account failed</Message>
$ rc admin service-account info $USER_ALIAS spark-ak
✗ Service account 'spark-ak' not found # clean missNothing converges this by retrying, so the operator rejects
accessKey == user up front as a spec error, and the rustfs-resources
chart fails at render time. Pick any other name, or omit accessKey and
let the operator generate one.
Since a user's own credentials already work for S3, you can point an
application straight at the User username/password and skip AccessKey
entirely. That is a legitimate setup, and it avoids the password plumbing
described below. What you give up:
- One identity, one credential. Two applications sharing a user share a secret, and rotating it breaks both at once. Access keys give each consumer its own independently revocable pair.
- Independent lifecycle. Deleting an access key does not disturb the user; rotating the user's password does not disturb its access keys.
- Narrower scoping. An access key can carry an embedded policy that further restricts it below the user's own permissions. A user credential always carries the user's full permissions.
- Expiry. Access keys can be given an expiration; user credentials cannot.
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.
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.
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; once it lands,
the password requirement and the per-user admin actions can both be dropped.
If operator behaviour ever looks wrong, check the server's view. Note that
service-account list defaults to the calling identity, so without
--user you are looking at the admin's own keys rather than the user's —
a common source of "my keys are missing":
rc admin user info <alias> spark # identity, status, policies
rc admin service-account list <alias> --user spark # that user's keys
rc admin service-account list <alias> # the alias's OWN keysThe 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.
- Passwords can be changed in place, contrary to what earlier versions
of these docs said. Re-issuing
admin user addfor an existing user replaces the password: the old one immediately returnsSignatureDoesNotMatch, while attached policies and existing access keys survive untouched. The operator nonetheless appliesspec.passwordonly at creation — a deliberate choice, not a server limitation — so editing the password Secret does not currently rotate anything. - Policy attachment is replace-all. RustFS's
set-user-or-group-policyendpoint has no attach/detach split, soUser.spec.policiesis fully declarative: whatever you list is exactly what ends up attached. - Attached policies cannot be deleted. Deleting a
Policystill referenced by a user fails; the CR reports the error and retries until nothing references it. - Access key length limits. AK is at most 20 characters, SK at most 40.
Exceeding either returns
InvalidAccessKeyLength.