Per-bucket object storage credentials with key rotation (Ceph RGW accounts) - #14170
Open
MitchDrage wants to merge 2 commits into
Open
MitchDrage wants to merge 2 commits into
MitchDrage wants to merge 2 commits into
Conversation
|
Congratulations on your first Pull Request and welcome to the Apache CloudStack community! If you have any issues or are unsure about any anything please check our Contribution Guide (https://github.com/apache/cloudstack/blob/main/CONTRIBUTING.md)
|
|
This pull request has merge conflicts. Dear author, please fix the conflicts and sync your branch with the base branch. |
Author
|
PR for documentation has been added. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Object storage credentials are provisioned per CloudStack account today: every bucket an account owns shares one access/secret key pair, and there is no way to rotate it.
This PR introduces per-bucket keys, a rotation mechanism, and the migration from per-account to per-bucket keys.
Fixes #14167
Discussed on dev@: "[DISCUSS] Per-bucket credentials and key rotation for object storage".
This PR's changes:
How it works
bucket_credential(one dedicated backend identity per bucket) andbucket_credential_key(one row per key slot, secret encrypted with@Encrypt). A bucket without abucket_credentialrow behaves exactly as today (account-scoped key) - the schema change is additive and no data is migrated.bucket.access_key/bucket.secret_keycolumns become a mirror of the bucket's newest active key, so every existing consumer of them (BucketTO, the policy/versioning driver calls, the UI object browser) keeps working unchanged.ObjectStoreDriver(createBucketCredential,createBucketCredentialKey,removeBucketCredentialKey,deleteBucketCredential, plus the account-levelsupportsBucketCredentials/accountSupportsBucketCredentials/migrateAccountForBucketCredentials).BaseObjectStoreDriverImplprovides "unsupported" defaults, so MinIO and Cloudian are untouched; the Simulator gets a stateless fake so the service layer is exercisable in CI.rotateBucketKey,revokeBucketKey,migrateBucketCredential(user-level, bucket-scoped) and, for root/domain admins,migrateObjectStoreAccountandrotateObjectStoreAccountKey. The latter completes a migration: once no bucket of the account on a store still uses the account key, it issues CloudStack a fresh account key and revokes the old one, so a shared key copied before the migration no longer opens every bucket. It is refused while legacy buckets remain.listObjectStoragePoolsacceptsaccountidand then reports, per store, whether the store supports per-bucket credentials at all, whether the account is migrated and how many buckets still use the account key.listBucketsresponses gaincredentialscope(bucket/account) and akeyslist, and acceptcredentialscopeas a filter so the buckets still using the account credential can be listed directly; secret keys are marked sensitive for API-log redaction.object.storage.per.bucket.credentials(defaulttrue) applies only when an account is first given an identity on an object store. It never changes an account that already has one:true(default)falseaccountdetailsinlistAccountsresponses, and this credential is CloudStack's rather than the account's.So these rows are named with a reserved prefix,
objectstore-, held as a constant (ObjectStore.ACCOUNT_DETAIL_PREFIX) so that the driver writing them and the API layer agree on it, andApiDBUtils.getAccountDetailsdrops/hides rows carrying that prefix before the response is built. The secret is additionally encrypted withDBEncryptionUtil. The namespace is the only thing removed; every other account detail, including those existing providers already store, is returned exactly as before.Points for reviewers
These are the design decisions I've made along the way:
object.storage.per.bucket.credentialsdecides how an accountis set up the first time it uses an object store. A migrated account always gets per-bucket credentials for new
buckets, whatever the setting says. We can't revert an Account back to a shared credential as it would write that account's root key onto its bucket rows, where every user of the account can read it.
optionalColumnsoption, and a details list can now vary per record. Both are additive and inert elsewhere, but they are shared files.ApiDBUtils.getAccountDetailsnow strips the reserved namespace this feature writes. Nothing else is removed, but every account response goes through that method.listBucketsfor callers who pass its access check, as the account-scoped key does today. Marking them sensitive keeps them out of logs; hiding them from the response would break the object browser and existing automation.listBucketsgained a per-bucket gateway call for buckets still on the account credential of a migrated account, to decide whether the migrate action applies. Accounts with no migration record answer from the database alone.bucketCredentialsUnsupportedReason). It is a default method, so no provider has to implement it, but adding a method toObjectStoreEntitymeans an incremental build has to rebuild the implementing module.UI changes people will see
Buckets list
A bucket's Details tab
A bucket's Keys tab (new, only on buckets with their own credential; viewable/usable by the bucket owner, admins and domain admins)
A bucket's actions
An account's Object Storage tab (new, viewable/usable by admins and domain admins)
Infrastructure, Object Storage, a store's Details tab (root admins only)
infooraccountscapability on the store's admin credential, a gateway that predates RGW accounts, a provider that does not offer the feature, or an unreachable admin API.Events
Changes to shared UI code, and why they are needed
Four files outside this feature are touched. Each is additive and gated so they don't apply to any other section, but they are shared so they are listed here for completeness.
views/AutogenView.vueoptionalColumns: columns that stay unticked until a condition first holds, then are ticked once and left to the viewer thereafterconfig/router.jscolumnscomponents/view/DetailsTab.vuedetails()function is now called with the record:details(this.resource)components/view/ListView.vuecredentialscopecell through the translation table, so it reads "Per-Bucket" rather thanbucketcomponents/view/SearchView.vuecredentialscopeadded to the list of filters drawn as a dropdown, with its two optionsCeph RGW implementation - requires Ceph Squid (v19) or later
On plain RGW users (the existing CloudStack approach to RGW credentals), giving an existing bucket its own identity cannot move the ownership of the objects already in it. The admin REST API can relink a bucket to another owner, but only the
radosgw-admin bucket chowncommand rewrites the objects inside. CloudStack accesses the gateway over REST and S3 with no shell access to it, so it has no way to migrate ownership. That leaves migrated buckets with split ownership: public bucket policies would 403 on the old objects and the old key would retain access to them. RGW accounts remove the cause: the account owns every bucket and object regardless of which identity wrote it. So the Ceph driver maps:account_details), whose root user is the existing account-UUID RGW user;An account counts as unmigrated only when the gateway answers and says so. An unreachable admin API, a 403 while an admin credential's capabilities are being changed, or a probe that flaps during a rolling gateway upgrade all leave the migration record standing, since migration is permanent at the backend. Treating those as unmigrated would drop the account back to its shared credential and write the account root key onto its next bucket. Where a per-bucket credential genuinely cannot be issued, bucket creation fails with the gateway's error.
Verified against three RGW releases (single-container clusters, same script):
/admin/infoaccountscapabilityPOST /admin/accountAccountAlreadyExistsGET /admin/accountReef cannot support this at all: its account endpoint answers 405, and
radosgw-admin caps addrefuses theaccountscapability, so no credential on it could hold what the endpoint requires. Squid is the floor.The plugin registers its own signer (
RgwIamSigner) for IAM calls. The AWS Java SDK signs the request before the HTTP layer attaches the formContent-Type, so that header never reaches the signature, and Tentacle answers 403 to a request whose content type was unsigned.This code works out whether a store supports this by asking the gateway:
/admin/infomust answer, the account endpoint must not return 405, and the store's admin credential must hold bothinfoandaccounts(withaccountsalone the gateway answers 403 on/admin/info). The three causes are indistinguishable from outside, so a root admin refused a migration is told which one applies, for example "the object store's admin credential is missing the 'info' capability". The probe re-checks automatically, so upgrading a gateway is picked up with no CloudStack configuration change.Incidental fix
listBucketshas accepted anobjectstorageidparameter since 4.19.0 that seems to have never worked. It declaredentityType = StoragePoolResponse.class(primary storage), so the UUID could never resolve and the call failed with "entity does not exist"; andsearchForBucketsInternalnever read the parameter at all, so once the UUID did resolve every bucket was still returned. Both are fixed here: the entity type is corrected toObjectStoreResponse.class, and the search now applies the filter. The account's Object Storage tab relies on it for its "Show Buckets" link (root admin only, matching the parameter's existing authorization, which is unchanged).I haven't found an issue raised against this, but I needed this fixed for my tests to work so I have fixed it here.
Documentation
Operator documentation is being raised separately against the documentation repository, and will be linked here once it is up. It covers the capabilities the store's admin credential needs (
infoandaccounts), what account migration does and that it cannot be undone, and the stages an administrator works through.Open question: how long should the account's Object Storage tab existin the Account page?
The tab, and the migration it drives, exist only because deployments have accounts that predate per-bucket credentials, and because some object storage cannot support them. Both shrink over time. Once the providers CloudStack supports offer per-bucket credentials, and versions that cannot are out of support, there is a decision to make: deprecate support for the backends and versions that cannot do this (Ceph before Squid is already end of life), at which point new accounts are always per-bucket, the migration becomes a one-off upgrade concern, and this tab can go. One suggestion I could make is to add the Object Storage provider and version into the anonomised usage telemetry service that has been discussed recently. That way, when we have a view on how much Ceph Reef is out there, we can make an informed decision on when to stop supporting it.
Known limitations / follow-ups
bucket.secret_keystill holds the mirrored active secret in clear, as it does today; only the newbucket_credential_keytable is encrypted. Encrypting the existing column would mean migrating the rows already in it, and a rolling-upgrade window in which management servers disagree about the format, so changing it is left out of this PR. It would also not change what the API hands out, since that secret is already returned to callers who can see the bucket and changing that may be a breaking change.Types of changes
Feature/Enhancement Scale or Bug Severity
Trying it out
Documentation is being raised separately, so here is enough to exercise the feature.
What you need. A Ceph cluster running Squid (v19) or later with RGW. A single container is enough. The RGW admin user whose keys you register the object store with needs two capabilities, and it will look unsupported without both:
Register it in CloudStack as a Ceph object store in the normal way. Infrastructure -> Object Storage -> the store's Details tab reports whether it can provide per-bucket credentials, and what to resolve if it cannot. That readiness information is root admin only.
A new account (the short path). An account that has never used this store is set up for per-bucket credentials on its first bucket, with no migration step.
radosgw-admin user listshows the bucket's IAM user is gone.An existing account (the migration path). This is the path that matters for upgrades, and it is what the account's Object Storage tab walks an administrator through. Use an account that already has buckets on the store.
An unsupported store. Register a Reef (18.x) cluster, or drop the
accountscapability from the admin user of a Squid one. The account's Object Storage tab says the store cannot be migrated. As root admin, the store's Details tab names the cause; a domain admin is told to contact their platform administrator.Checking at the gateway.
Turning it off.
object.storage.per.bucket.credentials=falsemakes new accounts use the shared credential as before. Accounts already migrated stay migrated.How Has This Been Tested?
CephObjectStoreDriverImplTest(37) andBucketApiServiceImplTest(26), plus a newObjectStoreAccountDetailTest(3) and four cases added toQueryManagerImplTestfor the bucket filters.object.storage.per.bucket.credentials=false):migrateObjectStoreAccountadopts the RGW user as account root with its keys still valid;migrateBucketCredentialgives the existing bucket a dedicated key that reads the pre-migration object; then the same new-bucket sequence as above. 25/25.rotateObjectStoreAccountKeyrefused while a legacy bucket remains, then succeeds once none remain - old account key rejected by RGW, root user holds one new key, per-bucket keys unaffected, new buckets created with the rotated key are per-bucket, a second rotation succeeds. Per-store scoping verified: the same account reports legacy on the other two stores.test_02_bucket_key_rotationcase added totest_bucket.py, with the helpers it needs added tomarvin/lib/base.py. It creates a Simulator object store, then checks that a new bucket has one active key slot mirrored onto the bucket, that rotation fills the free slot with a different key and moves the mirror, that revoking leaves the other slot active, and that revoking the last active key is refused. Run against a management server on the simulator profile: both cases in the file pass.test_01_create_bucketwas failing before this PR, sincecreateBuckethas a requiredquotaparameter (from Add Resource Limits to Backups and Object Storage #10017 ?) that wasn't filled. The one-line fix is included here, as our own case was written from the same pattern.