This commit is contained in:
mheler 2026-08-02 03:57:57 +00:00 committed by GitHub
commit a62e955ad7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
35 changed files with 2347 additions and 99 deletions

View File

@ -29,6 +29,8 @@
index, so only objects written or restored after this upgrade will populate
the field. Existing objects are unaffected.
* RGW: New S3 Control APIs to apply PublicAccessBlock configuration to User Accounts.
* RGW: STS session tokens can now be sealed with AES-256-GCM. See the STS
documentation for keyring setup and switching ``rgw_sts_token_format``.
* ceph-volume: Raw BlueStore OSD preparation now pre-formats NVMe devices and
skips the slower BlueStore discard phase,reducing mkfs time on
very large namespaces.

View File

@ -477,6 +477,31 @@ as follows:
:command:`role-policy delete`
Remove the policy attached to a role
:command:`sts keyring init`
Initialize the STS token-sealing keyring in the Monitor config-key store.
Generates a key, or installs one supplied with ``--infile``. The keyring
seals and verifies ``aead`` format session tokens; ``legacy`` tokens use
the ``rgw_sts_key`` option or, when that is unset, the single key stored
by ``--legacy``, which ``--yes-i-really-mean-it`` allows replacing.
:command:`sts keyring rotate`
Prepend a new sealing key to the STS keyring. Generates a key, or installs
one supplied with ``--infile``. ``--max-keys`` bounds the keyring size;
``--max-keys=1`` discards the current sealing key and requires
``--yes-i-really-mean-it``.
:command:`sts keyring list`
List the key ids of the STS keyring. With ``--legacy``, print whether a
legacy key is stored and its sha256 digest.
:command:`sts keyring rm`
Remove the STS keyring key named by ``--key-id``. With ``--legacy``,
remove the stored legacy key instead.
:command:`sts keyring trim`
Remove the oldest STS keyring keys, keeping the sealing key and, with
``--max-keys``, at most that many keys.
:command:`reshard add`
Schedule a resharding of a bucket
@ -645,6 +670,21 @@ Options
Optional for listing operations to specify the max entries.
.. option:: --key-id=<id>
The STS keyring key id (40 hexadecimal characters). Required for
``sts keyring rm``.
.. option:: --max-keys=<count>
Optional for ``sts keyring rotate`` and ``sts keyring trim`` to keep at
most this many keys.
.. option:: --legacy
Operate on the single stored legacy key instead of the AEAD keyring.
Supported by ``sts keyring init``, ``list``, and ``rm``.
.. option:: --purge-data
When specified, user removal will also purge the user's data.

View File

@ -104,8 +104,341 @@ STS Configuration
The following configurable options have to be added for STS integration:
.. confval:: rgw_sts_key
.. confval:: rgw_sts_token_format
.. confval:: rgw_sts_keyring_refresh_interval
.. confval:: rgw_sts_max_session_duration
.. confval:: rgw_s3_auth_use_sts
AES-256-GCM session-token sealing
---------------------------------
The keyring that seals and verifies AES-256-GCM session tokens is stored in
the Monitor config-key store under ``rgw/sts/keys``, alongside other cluster
secrets, and is managed with ``radosgw-admin sts keyring`` commands. The
keyring seals only ``aead`` format tokens; the ``legacy`` format uses a
single key, managed with the same commands under ``--legacy`` and always
overridden by :confval:`rgw_sts_key` when that option is set (see `Storing
the legacy key in the config-key store`_). The stored value is a
whitespace-separated list of ``<key-id>=<key>`` entries,
where the key id is 40 hexadecimal characters (20 bytes) and the key is the
canonical padded base64 encoding of 32 random bytes. At most 16 entries are
accepted. The first entry seals new tokens; every entry verifies.
Each RGW reads the keyring through its Monitor session on a background
thread and caches an immutable snapshot, so token operations never wait on
the Monitor. The snapshot is refreshed every
:confval:`rgw_sts_keyring_refresh_interval` seconds (one minute by default),
so a rotated key propagates within one interval.
Monitor access
~~~~~~~~~~~~~~
An RGW reads the keyring and the stored legacy key with the
``config-key get`` Monitor command. ``mon 'profile rgw'`` grants that under
the ``rgw/`` prefix, as does ``mon 'allow *'``. Blanket capabilities such as
``mon 'allow rw'`` do not. ``ceph auth caps`` replaces a key's entire
capability set, so give all three:
.. prompt:: bash $
ceph auth caps client.rgw.<id> mon 'profile rgw' osd 'profile rgw' mgr 'profile rgw'
The keyring travels over each RGW's Monitor session. An RGW logs a
warning when that session permits insecure connection modes; prefer msgr2
secure mode on deployments that use token sealing.
New deployments
~~~~~~~~~~~~~~~
For a single cluster with no existing STS credentials, create the keyring
and enable AEAD sealing:
.. prompt:: bash $
radosgw-admin sts keyring init
ceph config set client.rgw rgw_sts_token_format aead
``sts keyring init`` generates one random key and prints its id. It refuses
to overwrite an existing keyring. :confval:`rgw_sts_token_format` is
runtime-updatable and does not require a restart. Verify the effective
settings on every daemon under `Verifying the running configuration`_ and
complete `End-to-end token verification`_ before admitting production
traffic.
Manually generated keys and multisite
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Every cluster whose RGWs issue or verify the same tokens must hold an
identical keyring, including all zones in the realm and all federating
realms: each cluster's config-key store is independent, and a cluster cannot
verify tokens sealed with a key its own store does not hold. Generate the
keyring once and install the same file on every cluster instead of running
a bare ``init`` per cluster. The key file is a secret; create it under
``umask 077`` so it is readable only by its owner:
.. prompt:: bash $
umask 077
echo "$(openssl rand -hex 20)=$(openssl rand -base64 32)" > sts-keyring.txt
radosgw-admin sts keyring init --infile=sts-keyring.txt
``--infile`` accepts the same entry format as the stored keyring and applies
the same strict validation (canonical base64, unique ids and key material,
at most 16 entries), so a malformed file is rejected before it reaches the
Monitors. To export the keyring of a cluster that used generated keys, for
example when another cluster joins the deployment later:
.. prompt:: bash $
umask 077
ceph config-key get rgw/sts/keys > sts-keyring.txt
Storing the legacy key in the config-key store
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The ``legacy`` token key can be stored at ``rgw/sts/legacy_key`` beside the
AEAD keyring instead of in the configuration database, where any client
with ordinary Monitor capabilities can read it. The same commands manage
it under ``--legacy``:
.. prompt:: bash $
radosgw-admin sts keyring init --legacy
radosgw-admin sts keyring list --legacy
``init --legacy`` generates a 16-character key, or installs one supplied
with ``--infile``; with ``--yes-i-really-mean-it`` it replaces a stored key
in one step, which invalidates all outstanding legacy tokens.
``list --legacy`` prints a sha256 digest of the stored key and never the
key itself; compare digests across clusters the way keyrings are compared
under `Manually generated keys and multisite`_. ``rm --legacy`` removes
the stored key. ``rotate`` and ``trim`` do not apply: legacy tokens carry
no key id, so only one legacy key can exist.
When :confval:`rgw_sts_key` is set it always takes precedence and the
stored key is ignored; RGW logs a warning when the two differ. To
migrate an existing deployment, store the current key with
``init --legacy --infile``, confirm the digest on every cluster, and then
unset ``rgw_sts_key`` everywhere, including any daemon-local configuration
files. Each RGW adopts the stored key within one
:confval:`rgw_sts_keyring_refresh_interval`. In multisite deployments,
install the same value on every cluster, as with the AEAD keyring.
Upgrading an existing deployment
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The default ``legacy`` mode issues AES-128-CBC tokens and accepts both legacy
and AES-256-GCM tokens. The ``aead`` mode issues and accepts only AES-256-GCM
tokens. Use this ordering when upgrading:
#. Upgrade every RGW that issues or verifies the credentials, in every zone
and federating realm. Keep :confval:`rgw_sts_token_format` at ``legacy`` and
use the deployment or orchestrator inventory to confirm that no old RGW,
including a stopped daemon that could later rejoin, remains. Use
``ceph versions`` in each cluster to verify the versions of running daemons.
Old binaries cannot verify AES-256-GCM tokens.
#. Install the same keyring in every cluster while all RGWs remain in
``legacy`` mode, as described in `Manually generated keys and
multisite`_:
.. prompt:: bash $
radosgw-admin sts keyring init --infile=sts-keyring.txt
#. Verify the keyring in every cluster and confirm that every RGW still
uses ``legacy`` mode. Resolve any daemon-specific, local-file,
command-line, or runtime overrides of the format before continuing.
#. Test the keyring before the fleet-wide cutover. Drain one upgraded RGW
from normal traffic, set a daemon-specific override, and verify that it uses
``aead`` mode:
.. prompt:: bash $
canary='client.rgw.<canary-daemon-id>'
ceph config set "$canary" rgw_sts_token_format aead
ceph config show "$canary" rgw_sts_token_format
Complete `End-to-end token verification`_ with credentials issued directly
through the canary's STS endpoint. Return the canary to the inherited
``legacy`` setting and verify it before restoring normal traffic:
.. prompt:: bash $
ceph config rm "$canary" rgw_sts_token_format
ceph config show "$canary" rgw_sts_token_format
#. In a coordinated maintenance change, enable AEAD in every cluster as close
together as possible:
.. prompt:: bash $
ceph config set client.rgw rgw_sts_token_format aead
Runtime configuration propagates asynchronously, so this cutover is not
atomic across daemons or clusters. During a partial cutover, AEAD-mode
RGWs reject legacy tokens that legacy-mode RGWs can still issue.
Existing legacy sessions must re-authenticate.
#. Verify ``aead`` mode everywhere, then complete `End-to-end token
verification`_.
#. Retain :confval:`rgw_sts_key` until the rollback window has closed. When it
is no longer needed, remove it from each scope where it was configured. For
example, if it was set at the ``client.rgw`` scope:
.. prompt:: bash $
ceph config rm client.rgw rgw_sts_key
Verifying the running configuration
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
``radosgw-admin sts keyring list`` prints key ids and never key material,
and it parses the stored keyring with the same checks the daemon applies,
so it doubles as a correctness check after a manual installation. To compare keyrings across clusters,
compare digests instead of printing the value:
.. prompt:: bash $
radosgw-admin sts keyring list
ceph config-key get rgw/sts/keys | sha256sum
ceph config show client.rgw.<daemon-id> rgw_sts_token_format
The digest must match on every cluster, and the format must have the value
required by the current migration step on every RGW. The raw
``ceph config-key get`` output contains the secret keyring, so digest it
rather than copying it to logs or tickets.
End-to-end token verification
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Issue temporary security credentials through every STS endpoint that is
expected to issue them, then use each returned session token for an S3 request
through every RGW endpoint that is expected to verify it. This confirms
token issuance and cross-daemon verification with the effective runtime
configuration.
Waiting out issued tokens
~~~~~~~~~~~~~~~~~~~~~~~~~
Rollback and key retirement wait for previously issued tokens to expire. Role
credentials can currently last up to 43,200 seconds;
:confval:`rgw_sts_max_session_duration` governs only ``GetSessionToken`` and is
not by itself the wait bound.
Rolling back to legacy sealing
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Rollback must finish before any RGW is downgraded to a binary that does not
support AES-256-GCM tokens:
#. Ensure that the same original :confval:`rgw_sts_key` is still available to
every RGW. This option is not runtime-updatable. If it was removed, set
it again in every cluster, restart every RGW with the deployment's normal
restart procedure, and verify the effective value before continuing. For
example, compare digests without printing the key:
.. prompt:: bash $
rgw_daemon='client.rgw.<daemon-id>'
legacy_key='<original-legacy-key>'
ceph config set client.rgw rgw_sts_key "$legacy_key"
printf '%s\n' "$legacy_key" | sha256sum
ceph config show "$rgw_daemon" rgw_sts_key | sha256sum
#. Set every upgraded RGW back to ``legacy`` mode in a coordinated change:
.. prompt:: bash $
ceph config set client.rgw rgw_sts_token_format legacy
Upgraded RGWs now issue legacy tokens but continue to accept both token
formats.
#. Verify ``legacy`` mode on every daemon. Keep the AEAD keyring and the
upgraded binaries in place for the maximum lifetime of any AES-256-GCM token
issued before the last daemon was confirmed in ``legacy`` mode, as described
in `Waiting out issued tokens`_.
#. After all AES-256-GCM tokens have expired, old RGW binaries may be
installed. The keyring can then be removed:
.. prompt:: bash $
ceph config-key rm rgw/sts/keys
Rotating AEAD keys
~~~~~~~~~~~~~~~~~~
``sts keyring rotate`` prepends a new key, which seals all new tokens, and
preserves every existing entry for verification. Each RGW adopts the new
key on its next scheduled refresh, so during a rotation an RGW that has
already refreshed may seal a token with the new key that an RGW which has
not refreshed cannot yet verify. This window closes within one
:confval:`rgw_sts_keyring_refresh_interval`; the previous key stays valid
throughout, so already-issued sessions are unaffected.
.. note:: The keyring commands (``init``, ``rotate``, ``rm``, and ``trim``)
read the stored keyring, modify it, and write it back. The config-key store
has no compare-and-swap, so two commands running against the same cluster at
once can lose one of their changes. Run keyring commands serially; do not
invoke them concurrently on the same cluster.
On a single cluster, rotate with a generated key:
.. prompt:: bash $
radosgw-admin sts keyring rotate
In a multisite deployment, generate the new key once and rotate every
cluster with the same entry:
.. prompt:: bash $
umask 077
echo "$(openssl rand -hex 20)=$(openssl rand -base64 32)" > new-key.txt
radosgw-admin sts keyring rotate --infile=new-key.txt
Run the rotation on all clusters promptly, ideally within one keyring
refresh interval (one minute). A cluster can only learn keys from its own
config-key store, so until its rotation lands, it rejects tokens that
another cluster already seals with the new key. Sealing adopts a new key
only on a cache refresh, so completing all rotations within the interval
avoids this window in practice.
The keyring holds at most 16 entries. ``rotate --max-keys=<count>``
bounds its size in the same step by dropping the oldest entries beyond
``<count>`` after prepending the new key; use a bound large enough to keep
every key that may still have live tokens. ``rotate --max-keys=1`` discards
the current sealing key, invalidating all outstanding tokens, and requires
``--yes-i-really-mean-it``. If the keyring is already full,
first retire a verification key whose tokens have all expired. Never remove
a key that may still have live tokens merely to make room.
An RGW that has not refreshed its cached keyring yet keeps sealing with
the old key for up to one refresh interval (one minute) after its cluster's
rotation. Starting from the last cluster's rotation plus that interval, wait
for the maximum lifetime of any token issued under the old key, as described
in `Waiting out issued tokens`_. Then retire the oldest key everywhere:
.. prompt:: bash $
radosgw-admin sts keyring trim
``trim`` removes the oldest entry and never the sealing key;
``trim --max-keys=<count>`` instead drops as many of the oldest entries
as needed to leave at most ``<count>``, and ``rm --key-id=<id>`` removes one
specific key.
Removing a key invalidates tokens that it sealed. If a key is exposed,
remove it as soon as the replacement has propagated instead of retaining it
for the normal wait period; every RGW that still trusts the exposed key
remains forgeable.
.. warning:: The keys in ``rgw/sts/keys`` authenticate every AEAD session
token: anyone who obtains one can forge a valid token for any user or role,
just as a leaked ``rgw_sts_key`` allows forging legacy tokens. The
config-key store is not readable through blanket Monitor capabilities, and
``radosgw-admin sts keyring list`` never prints key material. Keep exported
keyring files out of plaintext repositories and logs, and distribute them
to every zone and realm over a secure channel.
.. note:: The STS and S3 APIs co-exist in the same namespace, and both S3
and STS APIs can be accessed via the same endpoint.

View File

@ -2,6 +2,7 @@
# https://tracker.ceph.com/issues/51282
# https://tracker.ceph.com/issues/61168
# https://tracker.ceph.com/issues/62504
# https://tracker.ceph.com/issues/78672
overrides:
ceph:
log-ignorelist:
@ -9,3 +10,4 @@ overrides:
- \(PG_DEGRADED\)
- \(POOL_APP_NOT_ENABLED\)
- not have an application enabled
- rgw is configured to optionally allow insecure connections to the monitors

View File

@ -0,0 +1,7 @@
tasks:
- exec:
client.0:
- sudo radosgw-admin sts keyring init
- sudo radosgw-admin sts keyring rotate
- sudo radosgw-admin sts keyring list
- sleep 5

View File

@ -0,0 +1,5 @@
tasks:
- workunit:
clients:
client.0:
- rgw/test_rgw_sts_keyring.sh

View File

@ -0,0 +1 @@
../.qa/

View File

@ -0,0 +1,6 @@
overrides:
ceph:
conf:
client:
rgw sts token format: aead
rgw sts keyring refresh interval: 1

View File

@ -63,7 +63,7 @@ def generate_caps(type_):
mds='allow',
),
client=dict(
mon='allow rw',
mon='allow rw, allow command "config-key get" with key prefix rgw/',
mgr='allow r',
osd='allow rwx',
mds='allow',

View File

@ -0,0 +1,97 @@
#!/usr/bin/env bash
#
# exercise the sts keyring lifecycle against a live cluster
#
set -ex
list() {
radosgw-admin sts keyring list
}
count_keys() {
list | python3 -c 'import json,sys; print(len(json.load(sys.stdin)))'
}
sealing_key() {
list | python3 -c 'import json,sys; print(next(k["key_id"] for k in json.load(sys.stdin) if k["seals"]))'
}
oldest_key() {
list | python3 -c 'import json,sys; print(json.load(sys.stdin)[-1]["key_id"])'
}
# the suite installs one key and rotates once before the s3 tests
count=$(count_keys)
seal=$(sealing_key)
# rotate with a generated key
radosgw-admin sts keyring rotate
[ "$(count_keys)" -eq $((count + 1)) ]
[ "$(sealing_key)" != "$seal" ]
seal=$(sealing_key)
# rotate with a supplied key
keyfile=$(mktemp)
trap 'rm -f "$keyfile"' EXIT
printf '%s=%s\n' "$(openssl rand -hex 20)" "$(openssl rand -base64 32)" > "$keyfile"
radosgw-admin sts keyring rotate --infile="$keyfile"
[ "$(count_keys)" -eq $((count + 2)) ]
[ "$(sealing_key)" != "$seal" ]
# rotate --max-keys=1 discards the sealing key, so it must ask first
if radosgw-admin sts keyring rotate --max-keys=1; then
exit 1
fi
[ "$(count_keys)" -eq $((count + 2)) ]
# rotate --max-keys retires the oldest keys in the same step
radosgw-admin sts keyring rotate --max-keys=2
[ "$(count_keys)" -eq 2 ]
# removing the sealing key must ask first
if radosgw-admin sts keyring rm --key-id "$(sealing_key)"; then
exit 1
fi
# removing an unknown key fails
if radosgw-admin sts keyring rm --key-id ffffffffffffffffffffffffffffffffffffffff; then
exit 1
fi
# retire the oldest verification key
radosgw-admin sts keyring rm --key-id "$(oldest_key)"
[ "$(count_keys)" -eq 1 ]
# the only key can never be removed
if radosgw-admin sts keyring rm --key-id "$(sealing_key)" --yes-i-really-mean-it; then
exit 1
fi
# confirmed rotate --max-keys=1 replaces the whole keyring
seal=$(sealing_key)
radosgw-admin sts keyring rotate --max-keys=1 --yes-i-really-mean-it
[ "$(count_keys)" -eq 1 ]
[ "$(sealing_key)" != "$seal" ]
# trim never removes the sealing key
radosgw-admin sts keyring trim | grep 'nothing to trim'
# the stored legacy key: store, inspect, guard replacement, remove
radosgw-admin sts keyring init --legacy
radosgw-admin sts keyring list --legacy | python3 -c '
import json, sys
out = json.load(sys.stdin)
assert out["present"]
assert len(out["sha256"]) == 64
'
if radosgw-admin sts keyring init --legacy; then
exit 1
fi
radosgw-admin sts keyring init --legacy --yes-i-really-mean-it
radosgw-admin sts keyring rm --legacy --yes-i-really-mean-it
radosgw-admin sts keyring list --legacy | python3 -c '
import json, sys
assert not json.load(sys.stdin)["present"]
'
echo OK

View File

@ -3859,10 +3859,48 @@ options:
This key must consist of 16 hexadecimal characters, which can be
generated by the command 'openssl rand -hex 8'. All radosgw instances
in a zone should use the same key. In multisite configurations, all
zones in a realm should use the same key.
zones in a realm should use the same key. When set, this option takes
precedence over the legacy key stored in the mon config-key store at
rgw/sts/legacy_key.
see_also:
- rgw_sts_token_format
services:
- rgw
with_legacy: true
- name: rgw_sts_token_format
type: str
level: advanced
desc: Seal used for newly issued STS session tokens
long_desc: legacy seals with AES-128-CBC and accepts both token formats. aead seals
and accepts only AES-256-GCM, rejecting legacy tokens. Initialize the keyring
in the mon config-key store with 'radosgw-admin sts keyring init' before
selecting aead.
default: legacy
see_also:
- rgw_sts_key
enum_values:
- legacy
- aead
services:
- rgw
flags:
- runtime
with_legacy: true
- name: rgw_sts_keyring_refresh_interval
type: int
level: advanced
desc: How often each radosgw refreshes the cached STS token-sealing keyring
long_desc: Each radosgw reads the keyring from the mon config-key store at
rgw/sts/keys on a background thread this many seconds apart, so a rotated
key propagates within one interval. Takes effect on daemon startup.
default: 60
min: 1
see_also:
- rgw_sts_token_format
services:
- rgw
flags:
- startup
# should we try to use sts for s3?
- name: rgw_s3_auth_use_sts
type: bool

View File

@ -138,6 +138,7 @@ set(librgw_common_srcs
rgw_object_lock.cc
rgw_kms.cc
rgw_kms_cache.cc
rgw_sts_keyring_cache.cc
rgw_kmip_client.cc
rgw_url.cc
rgw_oidc_provider.cc

View File

@ -84,6 +84,7 @@ extern "C" {
#include "rgw_lua.h"
#include "rgw_sal.h"
#include "rgw_sal_config.h"
#include "rgw_sts_keyring.h"
#include "rgw_data_access.h"
#include "rgw_account.h"
#include "rgw_bucket_logging.h"
@ -91,6 +92,7 @@ extern "C" {
#include "rgw_dedup_filter.h"
#include "services/svc_sync_modules.h"
#include "services/svc_cls.h"
#include "services/svc_config_key.h"
#include "services/svc_bilog_rados.h"
#include "services/svc_mdlog.h"
#include "services/svc_user.h"
@ -357,6 +359,11 @@ void usage()
cout << " mfa remove delete MFA TOTP token\n";
cout << " mfa check check MFA TOTP token\n";
cout << " mfa resync re-sync MFA TOTP token\n";
cout << " sts keyring init initialize the sts token-sealing keyring\n";
cout << " sts keyring rotate prepend a new key to the sts keyring\n";
cout << " sts keyring list list the key ids of the sts keyring\n";
cout << " sts keyring rm remove a key from the sts keyring\n";
cout << " sts keyring trim remove the oldest keys from the sts keyring\n";
#endif
cout << " topic list list bucket notifications topics\n";
cout << " topic get get a bucket notifications topic\n";
@ -557,6 +564,10 @@ void usage()
cout << " --totp-seconds the time resolution that is being used for TOTP generation\n";
cout << " --totp-window the number of TOTP tokens that are checked before and after the current token when validating token\n";
cout << " --totp-pin the valid value of a TOTP token at a certain time\n";
cout << "\nSTS keyring options:\n";
cout << " --key-id sts keyring key id (40 hexadecimal characters)\n";
cout << " --max-keys keep at most this many keys after sts keyring rotate/trim\n";
cout << " --legacy operate on the stored legacy sts key instead of the keyring\n";
cout << "\nBucket notifications options:\n";
cout << " --topic bucket notifications topic name\n";
cout << " --notification-id bucket notifications id\n";
@ -967,6 +978,11 @@ enum class OPT {
MFA_LIST,
MFA_CHECK,
MFA_RESYNC,
STS_KEYRING_INIT,
STS_KEYRING_ROTATE,
STS_KEYRING_LIST,
STS_KEYRING_RM,
STS_KEYRING_TRIM,
RESHARD_STALE_INSTANCES_LIST,
RESHARD_STALE_INSTANCES_DELETE,
RESHARDLOG_LIST,
@ -1268,6 +1284,11 @@ static SimpleCmd::Commands all_cmds = {
{ "mfa list", OPT::MFA_LIST },
{ "mfa check", OPT::MFA_CHECK },
{ "mfa resync", OPT::MFA_RESYNC },
{ "sts keyring init", OPT::STS_KEYRING_INIT },
{ "sts keyring rotate", OPT::STS_KEYRING_ROTATE },
{ "sts keyring list", OPT::STS_KEYRING_LIST },
{ "sts keyring rm", OPT::STS_KEYRING_RM },
{ "sts keyring trim", OPT::STS_KEYRING_TRIM },
{ "reshard stale-instances list", OPT::RESHARD_STALE_INSTANCES_LIST },
{ "reshard stale list", OPT::RESHARD_STALE_INSTANCES_LIST },
{ "reshard stale-instances delete", OPT::RESHARD_STALE_INSTANCES_DELETE },
@ -3720,6 +3741,280 @@ void init_realm_param(CephContext *cct, string& var, std::optional<string>& opt_
}
}
#ifdef WITH_RADOSGW_RADOS
static int sts_legacy_key_command(OPT opt_cmd, RGWSI_ConfigKey *svc_config_key,
const string& infile,
bool yes_i_really_mean_it,
Formatter *formatter)
{
if (opt_cmd == OPT::STS_KEYRING_ROTATE || opt_cmd == OPT::STS_KEYRING_TRIM) {
cerr << "ERROR: the legacy key is a single key; run sts keyring init "
"--legacy --yes-i-really-mean-it to replace it" << std::endl;
return EINVAL;
}
const std::string legacy_key{rgw::sts::STS_LEGACY_KEY_CONFIG_KEY};
bufferlist bl;
int ret = svc_config_key->get(legacy_key, true, &bl);
if (ret < 0 && ret != -ENOENT) {
cerr << "failed to read the legacy sts key: " << cpp_strerror(-ret) << std::endl;
return -ret;
}
if (opt_cmd == OPT::STS_KEYRING_LIST) {
formatter->open_object_section("legacy_key");
formatter->dump_bool("present", ret == 0);
if (ret == 0) {
const auto digest =
calc_hash_sha256(std::string_view{bl.c_str(), bl.length()});
formatter->dump_string("sha256", digest.to_str());
}
formatter->close_section();
formatter->flush(cout);
bl.zero();
return 0;
}
bl.zero();
if (opt_cmd == OPT::STS_KEYRING_RM) {
if (ret == -ENOENT) {
cerr << "ERROR: no legacy sts key is stored" << std::endl;
return ENOENT;
}
if (! yes_i_really_mean_it) {
cerr << "ERROR: removing the legacy key invalidates all outstanding "
"legacy tokens unless rgw_sts_key is set. Pass "
"--yes-i-really-mean-it to proceed." << std::endl;
return EINVAL;
}
if (int r = svc_config_key->rm(legacy_key); r < 0) {
cerr << "failed to remove the legacy sts key: " << cpp_strerror(-r) << std::endl;
return -r;
}
cout << "removed the legacy key" << std::endl;
return 0;
}
// init
if (ret == 0 && ! yes_i_really_mean_it) {
cerr << "ERROR: a legacy sts key is already stored; replacing it "
"invalidates all outstanding legacy tokens. Pass "
"--yes-i-really-mean-it to proceed." << std::endl;
return EEXIST;
}
std::string key;
if (! infile.empty()) {
bufferlist input;
if (int r = read_input(infile, input); r < 0) {
cerr << "ERROR: failed to read infile: " << cpp_strerror(-r) << std::endl;
return -r;
}
key = input.to_str();
input.zero();
rgw::sts::trim_legacy_key(key);
} else {
key = gen_rand_alphanumeric(g_ceph_context, 16);
}
// match the validation the daemon applies when loading the key
auto* cryptohandler = g_ceph_context->get_crypto_handler(CEPH_CRYPTO_AES);
bufferptr secret{key.c_str(), static_cast<unsigned>(key.size())};
if (! cryptohandler || cryptohandler->validate_secret(secret) < 0) {
ceph_memzero_s(key.data(), key.size(), key.size());
cerr << "ERROR: the legacy sts key must be at least 16 characters" << std::endl;
return EINVAL;
}
bufferlist out_bl;
out_bl.append(key);
ceph_memzero_s(key.data(), key.size(), key.size());
int r = svc_config_key->set(legacy_key, out_bl, true);
out_bl.zero();
if (r < 0) {
cerr << "failed to write the legacy sts key: " << cpp_strerror(-r) << std::endl;
return -r;
}
cout << (ret == 0 ? "replaced the legacy key" : "stored the legacy key")
<< std::endl;
return 0;
}
static int sts_keyring_command(OPT opt_cmd, RGWSI_ConfigKey *svc_config_key,
const string& infile, const string& key_id,
int max_keys, bool yes_i_really_mean_it,
Formatter *formatter)
{
using rgw::sts::StsKeyring;
using rgw::sts::sts_aead_key;
using rgw::sts::STS_AEAD_KEY_ID_SIZE;
using rgw::sts::STS_AEAD_KEY_SIZE;
using rgw::sts::hex_id;
using rgw::sts::parse_hex_id;
const std::string config_key{rgw::sts::STS_KEYRING_CONFIG_KEY};
bufferlist bl;
int ret = svc_config_key->get(config_key, true, &bl);
if (ret < 0 && ret != -ENOENT) {
cerr << "failed to read the sts keyring: " << cpp_strerror(-ret) << std::endl;
return -ret;
}
if (opt_cmd == OPT::STS_KEYRING_INIT) {
if (ret == 0) {
cerr << "ERROR: the sts keyring is already initialized" << std::endl;
return EEXIST;
}
} else if (ret == -ENOENT) {
cerr << "ERROR: the sts keyring is not initialized; run sts keyring init" << std::endl;
return ENOENT;
}
StsKeyring keyring;
std::string err;
// parse a keyring value, wiping the plaintext
const auto parse_keyring_bl = [&](bufferlist& in, StsKeyring& out) -> int {
std::string text = in.to_str();
in.zero();
const int r = StsKeyring::parse(text, out, err);
ceph_memzero_s(text.data(), text.size(), text.size());
if (r < 0) {
cerr << "ERROR: " << err << std::endl;
}
return r;
};
if (ret == 0) {
if (ret = parse_keyring_bl(bl, keyring); ret < 0) {
return -ret;
}
}
std::vector<std::string> added;
std::vector<std::string> removed;
const auto read_keyring_file = [&](StsKeyring& out) -> int {
bufferlist input;
if (int r = read_input(infile, input); r < 0) {
cerr << "ERROR: failed to read infile: " << cpp_strerror(-r) << std::endl;
return r;
}
return parse_keyring_bl(input, out);
};
if (opt_cmd == OPT::STS_KEYRING_INIT && ! infile.empty()) {
// install the supplied keyring verbatim
if (ret = read_keyring_file(keyring); ret < 0) {
return -ret;
}
for (const auto& key : keyring.entries()) {
added.push_back(hex_id(key.id));
}
} else if (opt_cmd == OPT::STS_KEYRING_INIT ||
opt_cmd == OPT::STS_KEYRING_ROTATE) {
if (opt_cmd == OPT::STS_KEYRING_ROTATE && max_keys == 1 &&
! yes_i_really_mean_it) {
cerr << "ERROR: rotate --max-keys=1 discards the current sealing key "
"and invalidates all outstanding tokens. Pass "
"--yes-i-really-mean-it to proceed." << std::endl;
return EINVAL;
}
/*
* rotate --max-keys caps the size, so drop enough old keys first to
* leave room for the new one
*/
if (opt_cmd == OPT::STS_KEYRING_ROTATE && max_keys > 0) {
for (auto& id : keyring.trim(static_cast<size_t>(max_keys) - 1)) {
removed.push_back(hex_id(id));
}
}
sts_aead_key newkey;
if (! infile.empty()) {
StsKeyring incoming;
if (ret = read_keyring_file(incoming); ret < 0) {
return -ret;
}
if (incoming.size() != 1) {
cerr << "ERROR: sts keyring rotate expects exactly one key entry" << std::endl;
return EINVAL;
}
auto single = incoming.release();
newkey = std::move(single[0]);
} else {
char buf[STS_AEAD_KEY_ID_SIZE + STS_AEAD_KEY_SIZE];
g_ceph_context->random()->get_bytes(buf, sizeof(buf));
newkey.id.assign(buf, STS_AEAD_KEY_ID_SIZE);
newkey.key.assign(buf + STS_AEAD_KEY_ID_SIZE, STS_AEAD_KEY_SIZE);
ceph_memzero_s(buf, sizeof(buf), sizeof(buf));
}
const std::string new_hex = hex_id(newkey.id);
if (int r = keyring.prepend(std::move(newkey), err); r < 0) {
cerr << "ERROR: " << err << std::endl;
return -r;
}
added.push_back(new_hex);
} else if (opt_cmd == OPT::STS_KEYRING_RM) {
if (key_id.empty()) {
cerr << "ERROR: key id was not provided (via --key-id)" << std::endl;
return EINVAL;
}
std::string raw_id;
if (parse_hex_id(key_id, raw_id) < 0) {
cerr << "ERROR: --key-id must be 40 hexadecimal characters" << std::endl;
return EINVAL;
}
if (keyring.size() > 1 && raw_id == keyring.sealing_key().id &&
!yes_i_really_mean_it) {
cerr << "ERROR: key " << key_id << " currently seals new tokens; "
"removing it invalidates all outstanding tokens. Pass "
"--yes-i-really-mean-it to proceed." << std::endl;
return EINVAL;
}
if (int r = keyring.remove(raw_id, err); r < 0) {
cerr << "ERROR: " << err << std::endl;
return -r;
}
removed.push_back(key_id);
} else if (opt_cmd == OPT::STS_KEYRING_TRIM) {
size_t keep = keyring.size() > 1 ? keyring.size() - 1 : 1;
if (max_keys > 0) {
keep = static_cast<size_t>(max_keys);
}
for (auto& id : keyring.trim(keep)) {
removed.push_back(hex_id(id));
}
if (removed.empty()) {
cout << "nothing to trim" << std::endl;
return 0;
}
}
if (opt_cmd == OPT::STS_KEYRING_LIST) {
formatter->open_array_section("keys");
for (const auto& key : keyring.entries()) {
formatter->open_object_section("key");
formatter->dump_string("key_id", hex_id(key.id));
formatter->dump_bool("seals", &key == &keyring.sealing_key());
formatter->close_section();
}
formatter->close_section();
formatter->flush(cout);
return 0;
}
std::string text = keyring.format();
bufferlist out_bl;
out_bl.append(text);
ceph_memzero_s(text.data(), text.size(), text.size());
ret = svc_config_key->set(config_key, out_bl, true);
out_bl.zero();
if (ret < 0) {
cerr << "failed to write the sts keyring: " << cpp_strerror(-ret) << std::endl;
return -ret;
}
for (const auto& id : added) {
cout << "added key " << id << std::endl;
}
for (const auto& id : removed) {
cout << "removed key " << id << std::endl;
}
return 0;
}
#endif
// This has an uncaught exception. Even if the exception is caught, the program
// would need to be terminated, so the warning is simply suppressed.
// coverity[root_function:SUPPRESS]
@ -3959,6 +4254,9 @@ int main(int argc, const char **argv)
boost::optional<std::string> compression_type;
string totp_serial;
string sts_key_id;
int sts_max_keys = -1;
int sts_legacy = false;
string totp_seed;
string totp_seed_type = "hex";
vector<string> totp_pin;
@ -4554,6 +4852,22 @@ int main(int argc, const char **argv)
description = val;
} else if (ceph_argparse_witharg(args, i, &val, "--totp-serial", (char*)NULL)) {
totp_serial = val;
} else if (ceph_argparse_witharg(args, i, &val, "--key-id", (char*)NULL)) {
sts_key_id = val;
} else if (ceph_argparse_witharg(args, i, &val, "--max-keys", (char*)NULL)) {
sts_max_keys = (int)strict_strtol(val.c_str(), 10, &err);
if (!err.empty()) {
cerr << "ERROR: failed to parse --max-keys: " << err << std::endl;
return EINVAL;
}
if (sts_max_keys < 1 ||
sts_max_keys > (int)rgw::sts::STS_AEAD_MAX_KEYS) {
cerr << "ERROR: --max-keys must be between 1 and "
<< rgw::sts::STS_AEAD_MAX_KEYS << std::endl;
return EINVAL;
}
} else if (ceph_argparse_binary_flag(args, i, &sts_legacy, NULL, "--legacy", (char*)NULL)) {
// do nothing
} else if (ceph_argparse_witharg(args, i, &val, "--totp-pin", (char*)NULL)) {
totp_pin.push_back(val);
} else if (ceph_argparse_witharg(args, i, &val, "--totp-seed", (char*)NULL)) {
@ -12093,6 +12407,24 @@ next:
}
if (opt_cmd == OPT::STS_KEYRING_INIT || opt_cmd == OPT::STS_KEYRING_ROTATE ||
opt_cmd == OPT::STS_KEYRING_LIST || opt_cmd == OPT::STS_KEYRING_RM ||
opt_cmd == OPT::STS_KEYRING_TRIM) {
if (driver->get_name() != "rados") {
cerr << "ERROR: this command is only available with the RADOS driver." << std::endl;
return EINVAL;
}
auto* svc_config_key =
static_cast<rgw::sal::RadosStore*>(driver)->svc()->config_key;
if (sts_legacy) {
return sts_legacy_key_command(opt_cmd, svc_config_key, infile,
yes_i_really_mean_it, formatter.get());
}
return sts_keyring_command(opt_cmd, svc_config_key, infile, sts_key_id,
sts_max_keys, yes_i_really_mean_it,
formatter.get());
}
if (opt_cmd == OPT::RESHARD_STALE_INSTANCES_LIST) {
if (!static_cast<rgw::sal::RadosStore*>(driver)->svc()->zone->can_reshard() && !yes_i_really_mean_it) {
cerr << "Resharding disabled in a multisite env, stale instances unlikely from resharding" << std::endl;

View File

@ -27,6 +27,7 @@
#include "include/str_list.h"
#include "include/stringify.h"
#include "rgw_kms_cache.h"
#include "rgw_sts_keyring_cache.h"
#include "rgw_main.h"
#include "rgw_asio_thread.h"
#include "rgw_common.h"
@ -292,7 +293,7 @@ void rgw::AppMain::cond_init_apis()
// S3 website mode is a specialization of S3
const bool s3website_enabled = apis_set.contains("s3website");
const bool s3control_enabled = apis_set.contains("s3control");
const bool sts_enabled = apis_set.contains("sts");
sts_api_enabled = apis_set.contains("sts");
const bool iam_enabled = apis_set.contains("iam");
const bool pubsub_enabled =
apis_set.contains("pubsub") || apis_set.contains("notifications");
@ -302,7 +303,7 @@ void rgw::AppMain::cond_init_apis()
if (!swift_at_root) {
rest.register_default_mgr(set_logging(
rest_filter(env.driver, RGW_REST_S3,
new RGWRESTMgr_S3(s3control_enabled, s3website_enabled, sts_enabled,
new RGWRESTMgr_S3(s3control_enabled, s3website_enabled, sts_api_enabled,
iam_enabled, pubsub_enabled))));
} else {
derr << "Cannot have the S3 or S3 Website enabled together with "
@ -538,6 +539,9 @@ int rgw::AppMain::init_frontends2(RGWLib* rgwlib)
if (dedup_background) {
rgw_pauser->add_pauser(dedup_background.get());
}
if (env.sts_keyring) {
rgw_pauser->add_pauser(env.sts_keyring.get());
}
reloader = std::make_unique<RGWRealmReloader>(
env, *implicit_tenant_context, service_map_meta, rgw_pauser.get(), *context_pool);
realm_watcher->add_watcher(RGWRealmNotify::Reload, *reloader);
@ -622,6 +626,27 @@ void rgw::AppMain::init_kms_cache()
dpp->get_cct(), Keyring::get_best());
}
void rgw::AppMain::init_sts_keyring()
{
// the cache serves both the STS API (issuing tokens) and S3 STS auth
// (verifying them), so build it whenever either is reachable
if (!have_http_frontend) {
return;
}
// the keyring lives in the mon config-key store; RadosStore only
if (env.driver->get_name() != "rados") {
return;
}
if (!sts_api_enabled && !g_conf().get_val<bool>("rgw_s3_auth_use_sts")) {
return;
}
const auto interval =
g_conf().get_val<int64_t>("rgw_sts_keyring_refresh_interval");
env.sts_keyring = std::make_unique<STS::KeyringCache>(
dpp->get_cct(), env.driver, std::chrono::seconds(interval));
env.sts_keyring->start();
}
void rgw::AppMain::shutdown(std::function<void(void)> finalize_async_signals)
{
// stop the realm reloader
@ -649,6 +674,9 @@ void rgw::AppMain::shutdown(std::function<void(void)> finalize_async_signals)
if (env.kms_cache) {
env.kms_cache->stop_ttl_reaper();
}
if (env.sts_keyring) {
env.sts_keyring->stop();
}
ldh.reset(nullptr); // deletes ldap helper if it was created
rgw_log_usage_finalize();

View File

@ -73,7 +73,7 @@ namespace rgw {
,8,6
> b64_iter;
while (sview.back() == '=')
while (!sview.empty() && sview.back() == '=')
sview.remove_suffix(1);
std::string outstr(b64_iter(sview.data()),

View File

@ -537,6 +537,7 @@ namespace rgw {
register_async_signal_handler(SIGUSR1, rgw::signal::handle_sigterm);
main.init_tracepoints();
main.init_sts_keyring();
r = main.init_frontends2(this /* rgwlib */);
if (r != 0) {
derr << "ERROR: unable to initialize frontend, r = " << r << dendl;

View File

@ -166,6 +166,7 @@ int main(int argc, char *argv[])
main.init_tracepoints();
main.init_lua();
main.init_kms_cache();
main.init_sts_keyring();
#ifdef WITH_RADOSGW_RADOS
main.init_dedup();
#endif

View File

@ -68,6 +68,8 @@ class AppMain {
* also serving HTTP */
bool have_http_frontend{false};
bool nfs{false};
// whether rgw_enable_apis includes sts
bool sts_api_enabled{false};
std::vector<RGWFrontend*> fes;
std::vector<RGWFrontendConfig*> fe_configs;
@ -141,6 +143,7 @@ public:
void init_tracepoints();
void init_lua();
void init_kms_cache();
void init_sts_keyring();
#ifdef WITH_RADOSGW_RADOS
void init_dedup();
#endif

View File

@ -7,6 +7,7 @@
#include "rgw_auth_registry.h"
#include "rgw_kms_cache.h"
#include "rgw_sts_keyring_cache.h"
class ActiveRateLimiter;
class OpsLogSink;
@ -52,6 +53,7 @@ struct RGWProcessEnv {
std::unique_ptr<rgw::auth::StrategyRegistry> auth_registry;
ActiveRateLimiter* ratelimiting = nullptr;
std::unique_ptr<rgw::kms::KMSCache> kms_cache;
std::unique_ptr<STS::KeyringCache> sts_keyring;
#ifdef WITH_ARROW_FLIGHT
// managed by rgw:flight::FlightFrontend in rgw_flight_frontend.cc

View File

@ -38,6 +38,7 @@
#include "rgw_rest.h"
#include "rgw_rest_s3.h"
#include "rgw_process_env.h"
#include "rgw_rest_s3control.h"
#include "rgw_rest_s3website.h"
#include "rgw_rest_pubsub.h"
@ -7340,61 +7341,6 @@ rgw::auth::s3::STSEngine::get_creds_info(const STS::SessionToken& token) const n
};
}
int
rgw::auth::s3::STSEngine::get_session_token(const DoutPrefixProvider* dpp, const std::string_view& session_token,
STS::SessionToken& token) const
{
string decodedSessionToken;
try {
decodedSessionToken = rgw::from_base64(session_token);
} catch (...) {
ldpp_dout(dpp, 0) << "ERROR: Invalid session token, not base64 encoded." << dendl;
return -EINVAL;
}
auto* cryptohandler = cct->get_crypto_handler(CEPH_CRYPTO_AES);
if (! cryptohandler) {
return -EINVAL;
}
string secret_s = cct->_conf->rgw_sts_key;
if (secret_s.empty()) {
ldpp_dout(dpp, 1) << "ERROR: rgw sts key not set" << dendl;
return -EINVAL;
}
buffer::ptr secret(secret_s.c_str(), secret_s.length());
int ret = 0;
if (ret = cryptohandler->validate_secret(secret); ret < 0) {
ldpp_dout(dpp, 0) << "ERROR: Invalid secret key" << dendl;
return -EINVAL;
}
string error;
std::unique_ptr<CryptoKeyHandler> keyhandler(cryptohandler->get_key_handler(secret, error));
if (! keyhandler) {
return -EINVAL;
}
error.clear();
string decrypted_str;
buffer::list en_input, dec_output;
en_input = buffer::list::static_from_string(decodedSessionToken);
ret = keyhandler->decrypt(en_input, dec_output, &error);
if (ret < 0) {
ldpp_dout(dpp, 0) << "ERROR: Decryption failed: " << error << dendl;
return -EPERM;
} else {
try {
dec_output.append('\0');
auto iter = dec_output.cbegin();
decode(token, iter);
} catch (const buffer::error& e) {
ldpp_dout(dpp, 0) << "ERROR: decode SessionToken failed: " << error << dendl;
return -EINVAL;
}
}
return 0;
}
rgw::auth::Engine::result_t
rgw::auth::s3::STSEngine::authenticate(
const DoutPrefixProvider* dpp,
@ -7414,7 +7360,7 @@ rgw::auth::s3::STSEngine::authenticate(
}
STS::SessionToken token;
if (int ret = get_session_token(dpp, session_token, token); ret < 0) {
if (int ret = STS::decode_session_token(dpp, cct, s->penv.sts_keyring.get(), session_token, token); ret < 0) {
return result_t::reject(ret);
}
//Authentication

View File

@ -1219,9 +1219,6 @@ class STSEngine : public AWSEngine {
acl_strategy_t get_acl_strategy() const { return nullptr; };
auth_info_t get_creds_info(const STS::SessionToken& token) const noexcept;
int get_session_token(const DoutPrefixProvider* dpp, const std::string_view& session_token,
STS::SessionToken& token) const;
result_t authenticate(const DoutPrefixProvider* dpp,
const std::string_view& access_key_id,
const std::string_view& signature,

View File

@ -39,6 +39,7 @@
#include "rgw_request.h"
#include "rgw_process.h"
#include "rgw_process_env.h"
#include "rgw_iam_policy.h"
#include "rgw_iam_policy_keywords.h"
@ -838,7 +839,7 @@ WebTokenEngine::authenticate( const DoutPrefixProvider* dpp,
int RGWREST_STS::verify_permission(optional_yield y)
{
STS::STSService _sts(s->cct, driver, s->user->get_id(), s->auth.identity.get());
STS::STSService _sts(s->cct, driver, s->penv.sts_keyring.get(), s->user->get_id(), s->auth.identity.get());
sts = std::move(_sts);
string rArn = s->info.args.get("RoleArn");
@ -937,7 +938,7 @@ void RGWSTSGetSessionToken::execute(optional_yield y)
return;
}
STS::STSService sts(s->cct, driver, s->user->get_id(), s->auth.identity.get());
STS::STSService sts(s->cct, driver, s->penv.sts_keyring.get(), s->user->get_id(), s->auth.identity.get());
STS::GetSessionTokenRequest req(duration, serialNumber, tokenCode);
const auto& [ret, creds] = sts.getSessionToken(this, req);

View File

@ -3,14 +3,20 @@
#include <errno.h>
#include <ctime>
#include <algorithm>
#include <memory>
#include <regex>
#include <string_view>
#include <boost/format.hpp>
#include <boost/algorithm/string/replace.hpp>
#include <openssl/evp.h>
#include "common/errno.h"
#include "common/Formatter.h"
#include "common/ceph_json.h"
#include "common/ceph_time.h"
#include "common/ceph_crypto.h"
#include "auth/Crypto.h"
#include "include/ceph_fs.h"
#include "common/iso_8601.h"
@ -25,14 +31,282 @@
#include "driver/rados/rgw_user.h"
#include "rgw_iam_policy.h"
#include "rgw_sts.h"
#include "rgw_sts_keyring.h"
#include "rgw_sts_keyring_cache.h"
#include "rgw_sal.h"
#define dout_subsys ceph_subsys_rgw
using namespace std;
namespace {
// AEAD-sealed tokens carry this prefix; legacy CBC tokens have none.
constexpr std::string_view STS_AEAD_PREFIX = "v2.";
constexpr size_t STS_AEAD_IV_SIZE = 12;
constexpr size_t STS_AEAD_TAG_SIZE = 16;
constexpr size_t STS_AEAD_SALT_SIZE = 32; // per-token key-derivation salt
constexpr unsigned char STS_AEAD_ZERO_IV[STS_AEAD_IV_SIZE] = {0};
using rgw::sts::STS_AEAD_KEY_ID_SIZE;
using rgw::sts::sts_aead_key;
// the sealed envelope prepends the key id and salt to the ciphertext
constexpr size_t STS_AEAD_HEADER_SIZE = STS_AEAD_KEY_ID_SIZE + STS_AEAD_SALT_SIZE;
bool sts_gcm_seal(const unsigned char* key,
const unsigned char* in, int in_len,
unsigned char* out, unsigned char* tag)
{
std::unique_ptr<EVP_CIPHER_CTX, decltype(&EVP_CIPHER_CTX_free)>
ctx{EVP_CIPHER_CTX_new(), EVP_CIPHER_CTX_free};
int len = 0;
if (! ctx ||
1 != EVP_EncryptInit_ex(ctx.get(), EVP_aes_256_gcm(), nullptr, nullptr, nullptr) ||
1 != EVP_CIPHER_CTX_ctrl(ctx.get(), EVP_CTRL_GCM_SET_IVLEN, STS_AEAD_IV_SIZE, nullptr) ||
1 != EVP_EncryptInit_ex(ctx.get(), nullptr, nullptr, key, STS_AEAD_ZERO_IV) ||
1 != EVP_EncryptUpdate(ctx.get(), out, &len, in, in_len)) {
return false;
}
int final_len = 0;
if (1 != EVP_EncryptFinal_ex(ctx.get(), out + len, &final_len) ||
1 != EVP_CIPHER_CTX_ctrl(ctx.get(), EVP_CTRL_GCM_GET_TAG, STS_AEAD_TAG_SIZE, tag)) {
return false;
}
return true;
}
bool sts_gcm_open(const unsigned char* key,
const unsigned char* in, int in_len,
const unsigned char* tag, unsigned char* out)
{
std::unique_ptr<EVP_CIPHER_CTX, decltype(&EVP_CIPHER_CTX_free)>
ctx{EVP_CIPHER_CTX_new(), EVP_CIPHER_CTX_free};
int len = 0;
if (! ctx ||
1 != EVP_DecryptInit_ex(ctx.get(), EVP_aes_256_gcm(), nullptr, nullptr, nullptr) ||
1 != EVP_CIPHER_CTX_ctrl(ctx.get(), EVP_CTRL_GCM_SET_IVLEN, STS_AEAD_IV_SIZE, nullptr) ||
1 != EVP_DecryptInit_ex(ctx.get(), nullptr, nullptr, key, STS_AEAD_ZERO_IV) ||
1 != EVP_DecryptUpdate(ctx.get(), out, &len, in, in_len) ||
1 != EVP_CIPHER_CTX_ctrl(ctx.get(), EVP_CTRL_GCM_SET_TAG, STS_AEAD_TAG_SIZE,
const_cast<unsigned char*>(tag))) {
return false;
}
int final_len = 0;
return 1 == EVP_DecryptFinal_ex(ctx.get(), out + len, &final_len);
}
/*
* Derive a per-token key from the salt so the GCM nonce can stay zero and
* still never repeat a (key, nonce) pair. Returns false, and wipes out, if the
* HMAC fails.
*/
bool sts_derive_token_key(const std::string& master,
const unsigned char* salt,
unsigned char (&out)[CEPH_CRYPTO_HMACSHA256_DIGESTSIZE])
{
try {
ceph::crypto::HMACSHA256 hmac(
reinterpret_cast<const unsigned char*>(master.data()), master.size());
hmac.Update(salt, STS_AEAD_SALT_SIZE);
hmac.Final(out);
} catch (const ceph::crypto::DigestException&) {
::ceph::crypto::zeroize_for_security(out, sizeof(out));
return false;
}
return true;
}
int get_sts_cbc_key_handler(const DoutPrefixProvider* dpp, CephContext* cct,
STS::KeyringCache* keyring_cache,
std::unique_ptr<CryptoKeyHandler>& out)
{
auto* cryptohandler = cct->get_crypto_handler(CEPH_CRYPTO_AES);
if (! cryptohandler) {
ldpp_dout(dpp, 0) << "ERROR: No AES crypto handler found !" << dendl;
return -EINVAL;
}
// rgw_sts_key always takes precedence over the stored legacy key
std::string secret_s = cct->_conf.get_val<std::string>("rgw_sts_key");
if (secret_s.empty() && keyring_cache) {
if (const auto legacy = keyring_cache->get_legacy(); legacy) {
secret_s = *legacy;
}
}
if (secret_s.empty()) {
ldpp_dout(dpp, 1) << "ERROR: rgw sts key not set" << dendl;
return -EINVAL;
}
bufferptr secret(secret_s.c_str(), secret_s.length());
if (int ret = cryptohandler->validate_secret(secret); ret < 0) {
ldpp_dout(dpp, 0) << "ERROR: Invalid rgw sts key, please ensure it is an alphanumeric key of length 16" << dendl;
return ret;
}
std::string error;
out.reset(cryptohandler->get_key_handler(secret, error));
if (! out) {
ldpp_dout(dpp, 0) << "ERROR: No Key handler found !" << dendl;
return -EINVAL;
}
return 0;
}
int get_sts_keyring(const DoutPrefixProvider* dpp,
STS::KeyringCache* keyring_cache,
STS::KeyringSnapshot& out)
{
out = keyring_cache ? keyring_cache->get() : nullptr;
if (! out) {
ldpp_dout(dpp, 0) << "ERROR: the STS keyring is not available" << dendl;
return -EINVAL;
}
return 0;
}
bool sts_aead_token_format(CephContext* cct)
{
return cct->_conf.get_val<std::string>("rgw_sts_token_format") == "aead";
}
} // anonymous namespace
namespace STS {
int seal_session_token(const DoutPrefixProvider* dpp, CephContext* cct,
const sts_aead_key& active,
bufferlist plaintext, std::string& out)
{
unsigned char salt[STS_AEAD_SALT_SIZE];
cct->random()->get_bytes(reinterpret_cast<char*>(salt), sizeof(salt));
unsigned char key[CEPH_CRYPTO_HMACSHA256_DIGESTSIZE];
if (! sts_derive_token_key(active.key, salt, key)) {
ldpp_dout(dpp, 0) << "ERROR: session token key derivation failed" << dendl;
return -ERR_INTERNAL_ERROR;
}
const int in_len = static_cast<int>(plaintext.length());
std::string envelope;
envelope.reserve(STS_AEAD_HEADER_SIZE + in_len + STS_AEAD_TAG_SIZE);
envelope.append(active.id);
envelope.append(reinterpret_cast<const char*>(salt), STS_AEAD_SALT_SIZE);
const size_t cipher_off = envelope.size();
envelope.resize(cipher_off + in_len + STS_AEAD_TAG_SIZE);
auto* cipher = reinterpret_cast<unsigned char*>(envelope.data()) + cipher_off;
const bool ok = sts_gcm_seal(key,
reinterpret_cast<const unsigned char*>(plaintext.c_str()),
in_len, cipher, cipher + in_len);
::ceph::crypto::zeroize_for_security(key, sizeof(key));
if (! ok) {
ldpp_dout(dpp, 0) << "ERROR: AES-256-GCM sealing of session token failed" << dendl;
return -ERR_INTERNAL_ERROR;
}
out.assign(STS_AEAD_PREFIX);
out += rgw::to_base64(envelope);
return 0;
}
int unseal_session_token(const DoutPrefixProvider* dpp,
const rgw::sts::StsKeyring& keyring,
std::string_view token, bufferlist& plaintext)
{
std::string envelope;
try {
envelope = rgw::from_base64(token.substr(STS_AEAD_PREFIX.size()));
} catch (...) {
ldpp_dout(dpp, 0) << "ERROR: session token is not valid base64" << dendl;
return -EINVAL;
}
if (envelope.size() <= STS_AEAD_HEADER_SIZE + STS_AEAD_TAG_SIZE) {
ldpp_dout(dpp, 0) << "ERROR: session token is truncated" << dendl;
return -EINVAL;
}
const std::string_view key_id{envelope.data(), STS_AEAD_KEY_ID_SIZE};
const auto* buf = reinterpret_cast<const unsigned char*>(envelope.data());
const unsigned char* salt = buf + STS_AEAD_KEY_ID_SIZE;
const unsigned char* cipher = buf + STS_AEAD_HEADER_SIZE;
const int cipher_len = static_cast<int>(
envelope.size() - STS_AEAD_HEADER_SIZE - STS_AEAD_TAG_SIZE);
const unsigned char* tag = cipher + cipher_len;
const sts_aead_key* active = keyring.find(key_id);
if (! active) {
ldpp_dout(dpp, 0) << "ERROR: session token sealed with an unknown key id" << dendl;
return -EPERM;
}
unsigned char key[CEPH_CRYPTO_HMACSHA256_DIGESTSIZE];
if (! sts_derive_token_key(active->key, salt, key)) {
ldpp_dout(dpp, 0) << "ERROR: session token key derivation failed" << dendl;
return -ERR_INTERNAL_ERROR;
}
bufferptr opened(cipher_len);
const bool ok = sts_gcm_open(key, cipher, cipher_len, tag,
reinterpret_cast<unsigned char*>(opened.c_str()));
::ceph::crypto::zeroize_for_security(key, sizeof(key));
if (! ok) {
// this plaintext isn't authenticated, so wipe it
::ceph::crypto::zeroize_for_security(opened.c_str(), opened.length());
ldpp_dout(dpp, 0) << "ERROR: session token failed authentication" << dendl;
return -EPERM;
}
plaintext.clear();
plaintext.push_back(std::move(opened));
return 0;
}
int decode_session_token(const DoutPrefixProvider* dpp, CephContext* cct,
STS::KeyringCache* keyring_cache,
std::string_view session_token, SessionToken& token)
{
bufferlist plaintext;
if (session_token.starts_with(STS_AEAD_PREFIX)) {
STS::KeyringSnapshot keyring;
if (int ret = get_sts_keyring(dpp, keyring_cache, keyring); ret < 0) {
return ret;
}
if (int ret = unseal_session_token(dpp, *keyring, session_token, plaintext); ret < 0) {
return ret;
}
} else {
if (sts_aead_token_format(cct)) {
ldpp_dout(dpp, 0) << "ERROR: legacy session token rejected because rgw_sts_token_format is aead" << dendl;
return -EPERM;
}
std::string decoded;
try {
decoded = rgw::from_base64(session_token);
} catch (...) {
ldpp_dout(dpp, 0) << "ERROR: Invalid session token, not base64 encoded." << dendl;
return -EINVAL;
}
std::unique_ptr<CryptoKeyHandler> keyhandler;
if (int ret = get_sts_cbc_key_handler(dpp, cct, keyring_cache, keyhandler); ret < 0) {
return ret;
}
std::string error;
bufferlist encrypted = bufferlist::static_from_string(decoded);
if (int ret = keyhandler->decrypt(encrypted, plaintext, &error); ret < 0) {
ldpp_dout(dpp, 0) << "ERROR: Decryption failed: " << error << dendl;
return -EPERM;
}
plaintext.append('\0');
}
try {
auto iter = plaintext.cbegin();
decode(token, iter);
} catch (const buffer::error& e) {
ldpp_dout(dpp, 0) << "ERROR: decode SessionToken failed" << dendl;
return -EINVAL;
}
return 0;
}
void Credentials::dump(Formatter *f) const
{
encode_json("AccessKeyId", accessKeyId , f);
@ -43,6 +317,7 @@ void Credentials::dump(Formatter *f) const
int Credentials::generateCredentials(const DoutPrefixProvider *dpp,
CephContext* cct,
STS::KeyringCache* keyring_cache,
const uint64_t& duration,
const boost::optional<std::string>& policy,
const boost::optional<std::string>& roleId,
@ -68,31 +343,18 @@ int Credentials::generateCredentials(const DoutPrefixProvider *dpp,
real_clock::time_point exp = t + std::chrono::seconds(duration);
expiration = ceph::to_iso_8601(exp);
//Session Token - Encrypt using AES
auto* cryptohandler = cct->get_crypto_handler(CEPH_CRYPTO_AES);
if (! cryptohandler) {
ldpp_dout(dpp, 0) << "ERROR: No AES crypto handler found !" << dendl;
return -EINVAL;
}
string secret_s = cct->_conf->rgw_sts_key;
if (secret_s.empty()) {
ldpp_dout(dpp, 1) << "ERROR: rgw sts key not set" << dendl;
return -EINVAL;
}
buffer::ptr secret(secret_s.c_str(), secret_s.length());
int ret = 0;
if (ret = cryptohandler->validate_secret(secret); ret < 0) {
ldpp_dout(dpp, 0) << "ERROR: Invalid rgw sts key, please ensure it is an alphanumeric key of length 16" << dendl;
const bool aead = sts_aead_token_format(cct);
STS::KeyringSnapshot keyring;
std::unique_ptr<CryptoKeyHandler> keyhandler;
if (aead) {
if (int ret = get_sts_keyring(dpp, keyring_cache, keyring); ret < 0) {
return ret;
}
} else if (int ret = get_sts_cbc_key_handler(dpp, cct, keyring_cache,
keyhandler); ret < 0) {
return ret;
}
string error;
std::unique_ptr<CryptoKeyHandler> keyhandler(cryptohandler->get_key_handler(secret, error));
if (! keyhandler) {
ldpp_dout(dpp, 0) << "ERROR: No Key handler found !" << dendl;
return -EINVAL;
}
error.clear();
//Storing policy and roleId as part of token, so that they can be extracted
// from the token itself for policy evaluation.
SessionToken token;
@ -140,10 +402,17 @@ int Credentials::generateCredentials(const DoutPrefixProvider *dpp,
if (session_princ_tags) {
token.principal_tags = std::move(*session_princ_tags);
}
buffer::list input, enc_output;
buffer::list input;
encode(token, input);
if (ret = keyhandler->encrypt(input, enc_output, &error); ret < 0) {
if (aead) {
return seal_session_token(dpp, cct, keyring->sealing_key(), std::move(input), sessionToken);
}
string error;
buffer::list enc_output;
if (int ret = keyhandler->encrypt(input, enc_output, &error); ret < 0) {
ldpp_dout(dpp, 0) << "ERROR: Encrypting session token returned an error !" << dendl;
return ret;
}
@ -153,7 +422,7 @@ int Credentials::generateCredentials(const DoutPrefixProvider *dpp,
encoded_op.append('\0');
sessionToken = encoded_op.c_str();
return ret;
return 0;
}
void AssumedRoleUser::dump(Formatter *f) const
@ -381,7 +650,7 @@ AssumeRoleWithWebIdentityResponse STSService::assumeRoleWithWebIdentity(const Do
//Generate Credentials
//Role and Policy provide the authorization info, user id and applier info are not needed
response.assumeRoleResp.retCode = response.assumeRoleResp.creds.generateCredentials(dpp, cct, req.getDuration(),
response.assumeRoleResp.retCode = response.assumeRoleResp.creds.generateCredentials(dpp, cct, keyring_cache, req.getDuration(),
req.getPolicy(), roleId,
req.getRoleSessionName(),
token_claims,
@ -432,7 +701,7 @@ AssumeRoleResponse STSService::assumeRole(const DoutPrefixProvider *dpp,
//Generate Credentials
//Role and Policy provide the authorization info, user id and applier info are not needed
response.retCode = response.creds.generateCredentials(dpp, cct, req.getDuration(),
response.retCode = response.creds.generateCredentials(dpp, cct, keyring_cache, req.getDuration(),
req.getPolicy(), roleId,
req.getRoleSessionName(),
boost::none,
@ -463,7 +732,7 @@ GetSessionTokenResponse STSService::getSessionToken(const DoutPrefixProvider *dp
Credentials cred;
//Generate Credentials
if (ret = cred.generateCredentials(dpp, cct,
if (ret = cred.generateCredentials(dpp, cct, keyring_cache,
req.getDuration(),
boost::none,
boost::none,

View File

@ -3,10 +3,17 @@
#pragma once
#include <string_view>
#include "rgw_role.h"
#include "rgw_auth.h"
#include "rgw_web_idp.h"
namespace rgw::sts {
struct sts_aead_key;
class StsKeyring;
}
namespace STS {
class AssumeRoleRequestBase {
@ -189,6 +196,25 @@ struct SessionToken {
};
WRITE_CLASS_ENCODER(SessionToken)
class KeyringCache;
/*
* seal a token body under the keyring's sealing key. the "v2." envelope holds
* the key id, a random salt, the ciphertext and the GCM tag, base64 encoded
*/
int seal_session_token(const DoutPrefixProvider* dpp, CephContext* cct,
const rgw::sts::sts_aead_key& key,
bufferlist plaintext, std::string& out);
// verify and open a "v2." token against the keyring
int unseal_session_token(const DoutPrefixProvider* dpp,
const rgw::sts::StsKeyring& keyring,
std::string_view token, bufferlist& plaintext);
int decode_session_token(const DoutPrefixProvider* dpp, CephContext* cct,
KeyringCache* keyring_cache,
std::string_view session_token, SessionToken& token);
class Credentials {
static constexpr int MAX_ACCESS_KEY_LEN = 20;
static constexpr int MAX_SECRET_KEY_LEN = 40;
@ -199,6 +225,7 @@ class Credentials {
public:
int generateCredentials(const DoutPrefixProvider *dpp,
CephContext* cct,
KeyringCache* keyring_cache,
const uint64_t& duration,
const boost::optional<std::string>& policy,
const boost::optional<std::string>& roleId,
@ -235,14 +262,17 @@ using AssumeRoleWithWebIdentityResponse = struct AssumeRoleWithWebIdentityRespon
class STSService {
CephContext* cct;
rgw::sal::Driver* driver;
KeyringCache* keyring_cache = nullptr;
rgw_user user_id;
std::unique_ptr<rgw::sal::RGWRole> role;
rgw::auth::Identity* identity;
public:
STSService() = default;
STSService(CephContext* cct, rgw::sal::Driver* driver, rgw_user user_id,
STSService(CephContext* cct, rgw::sal::Driver* driver,
KeyringCache* keyring_cache, rgw_user user_id,
rgw::auth::Identity* identity)
: cct(cct), driver(driver), user_id(user_id), identity(identity) {}
: cct(cct), driver(driver), keyring_cache(keyring_cache),
user_id(user_id), identity(identity) {}
std::tuple<int, rgw::sal::RGWRole*> getRoleInfo(const DoutPrefixProvider *dpp, const std::string& arn, optional_yield y);
AssumeRoleResponse assumeRole(const DoutPrefixProvider *dpp, AssumeRoleRequest& req, optional_yield y);
GetSessionTokenResponse getSessionToken(const DoutPrefixProvider *dpp, GetSessionTokenRequest& req);

263
src/rgw/rgw_sts_keyring.h Normal file
View File

@ -0,0 +1,263 @@
// -*- mode:C++; tab-width:8; c-basic-offset:2; indent-tabs-mode:nil -*-
// vim: ts=8 sw=2 sts=2 expandtab ft=cpp
#pragma once
#include <algorithm>
#include <cerrno>
#include <cstddef>
#include <iterator>
#include <string>
#include <string_view>
#include <utility>
#include <vector>
#include "common/split.h"
#include "include/compat.h"
#include "rgw_b64.h"
#include "rgw_hex.h"
namespace rgw::sts {
// the token-sealing keyring lives in the mon config-key store
inline constexpr std::string_view STS_KEYRING_CONFIG_KEY = "rgw/sts/keys";
inline constexpr std::string_view STS_LEGACY_KEY_CONFIG_KEY = "rgw/sts/legacy_key";
inline constexpr std::size_t STS_AEAD_KEY_ID_SIZE = 20;
inline constexpr std::size_t STS_AEAD_KEY_SIZE = 32;
inline constexpr std::size_t STS_AEAD_MAX_KEYS = 16;
// 40-char hex form of a raw key id
inline std::string hex_id(const std::string& raw_id)
{
std::string hex;
buf_to_hex(raw_id, std::back_inserter(hex));
return hex;
}
/*
* decode a 40-char hex id into its raw bytes; hex_to_buf requires
* NUL-terminated input
*/
inline int parse_hex_id(const std::string& hex, std::string& raw_id)
{
raw_id.resize(STS_AEAD_KEY_ID_SIZE);
if (hex_to_buf(hex.c_str(), raw_id.data(),
static_cast<int>(raw_id.size())) !=
static_cast<int>(raw_id.size())) {
return -EINVAL;
}
return 0;
}
// strip trailing whitespace from a stored legacy key value
inline void trim_legacy_key(std::string& key)
{
if (const auto end = key.find_last_not_of(" \t\r\n"); end != key.npos) {
key.resize(end + 1);
} else {
key.clear();
}
}
/*
* a sealing key: a 20-byte id and a 32-byte key. the key is wiped when it is
* overwritten or destroyed
*/
struct sts_aead_key {
std::string id;
std::string key;
sts_aead_key() = default;
sts_aead_key(sts_aead_key&&) = default;
sts_aead_key& operator=(sts_aead_key&& other) noexcept
{
if (this != &other) {
ceph_memzero_s(key.data(), key.size(), key.size());
id = std::move(other.id);
key = std::move(other.key);
}
return *this;
}
~sts_aead_key()
{
ceph_memzero_s(key.data(), key.size(), key.size());
}
};
/*
* An ordered set of sealing keys. parse() and the mutation helpers keep it
* valid: at most 16 entries, 40-hex ids, keys that decode to 32 bytes from
* canonical base64, and no repeated ids or key material. The first entry
* seals new tokens; the rest are kept to verify older ones.
*/
class StsKeyring {
std::vector<sts_aead_key> keys;
static int decode_entry(std::string_view entry, sts_aead_key& key,
std::string& err)
{
const auto separator = entry.find('=');
if (separator == entry.npos) {
err = "sts keyring has an invalid entry";
return -EINVAL;
}
const auto id_hex = entry.substr(0, separator);
const auto encoded_key = entry.substr(separator + 1);
if (parse_hex_id(std::string{id_hex}, key.id) < 0) {
err = "sts keyring has a key id that is not 40 hexadecimal characters";
return -EINVAL;
}
try {
key.key = rgw::from_base64(encoded_key);
} catch (...) {
err = "sts keyring has a key that is not valid base64";
return -EINVAL;
}
if (key.key.size() != STS_AEAD_KEY_SIZE) {
err = "sts keyring has a key that does not decode to 32 bytes";
return -EINVAL;
}
std::string reencoded = rgw::to_base64(key.key);
const bool canonical = reencoded == encoded_key;
ceph_memzero_s(reencoded.data(), reencoded.size(), reencoded.size());
if (! canonical) {
err = "sts keyring has a key that is not canonical base64";
return -EINVAL;
}
return 0;
}
int check_unique(const sts_aead_key& key, std::string& err) const
{
for (const auto& existing : keys) {
if (existing.id == key.id) {
err = "sts keyring has a duplicate key id";
return -EINVAL;
}
if (existing.key == key.key) {
err = "sts keyring has duplicate key material";
return -EINVAL;
}
}
return 0;
}
public:
// parse whitespace-separated <hex-id>=<base64-key> entries
static int parse(std::string_view text, StsKeyring& out, std::string& err)
{
err.clear();
if (text.empty()) {
err = "sts keyring is empty";
return -EINVAL;
}
StsKeyring result;
result.keys.reserve(STS_AEAD_MAX_KEYS);
// ceph::split's default delimiter set includes '='; split on whitespace
for (std::string_view entry : ceph::split(text, " \t\r\n")) {
if (result.keys.size() == STS_AEAD_MAX_KEYS) {
err = "sts keyring accepts at most 16 keys";
return -EINVAL;
}
sts_aead_key key;
if (int r = decode_entry(entry, key, err); r < 0) {
return r;
}
if (int r = result.check_unique(key, err); r < 0) {
return r;
}
result.keys.push_back(std::move(key));
}
if (result.keys.empty()) {
err = "sts keyring contains no keys";
return -EINVAL;
}
out = std::move(result);
return 0;
}
// one <hex-id>=<base64-key> entry per line, in verification order
std::string format() const
{
std::string out;
for (const auto& key : keys) {
out += hex_id(key.id);
out.push_back('=');
out += rgw::to_base64(key.key);
out.push_back('\n');
}
return out;
}
std::size_t size() const { return keys.size(); }
const std::vector<sts_aead_key>& entries() const { return keys; }
// seals new tokens. only valid when the keyring isn't empty
const sts_aead_key& sealing_key() const { return keys.front(); }
const sts_aead_key* find(std::string_view id) const
{
for (const auto& key : keys) {
if (key.id == id) {
return &key;
}
}
return nullptr;
}
// prepend a new sealing key; rejects a duplicate or a full keyring
int prepend(sts_aead_key key, std::string& err)
{
if (keys.size() >= STS_AEAD_MAX_KEYS) {
err = "the sts keyring cannot hold more than 16 keys; retire one with"
" 'sts keyring trim' or 'sts keyring rm' first";
return -EINVAL;
}
if (int r = check_unique(key, err); r < 0) {
return r;
}
keys.insert(keys.begin(), std::move(key));
return 0;
}
// remove the key with this raw id; rejects an unknown id or the only key
int remove(std::string_view id, std::string& err)
{
auto it = std::find_if(keys.begin(), keys.end(),
[&](const sts_aead_key& k) { return k.id == id; });
if (it == keys.end()) {
err = "the sts keyring has no such key";
return -ENOENT;
}
if (keys.size() == 1) {
err = "refusing to remove the sts keyring's only key";
return -EINVAL;
}
keys.erase(it);
return 0;
}
/*
* drop the oldest entries beyond keep (keep may be 0); returns the raw ids
* removed
*/
std::vector<std::string> trim(std::size_t keep)
{
std::vector<std::string> removed;
while (keys.size() > keep) {
removed.push_back(keys.back().id);
keys.pop_back();
}
return removed;
}
// move the keys out, leaving the keyring empty
std::vector<sts_aead_key> release() { return std::move(keys); }
};
} // namespace rgw::sts

View File

@ -0,0 +1,165 @@
// -*- mode:C++; tab-width:8; c-basic-offset:2; indent-tabs-mode:nil -*-
// vim: ts=8 sw=2 sts=2 expandtab ft=cpp
#include "rgw_sts_keyring_cache.h"
#include <condition_variable>
#include <mutex>
#include "common/ceph_context.h"
#include "common/dout.h"
#include "common/errno.h"
#include "include/compat.h"
#include "rgw_sal.h"
#define dout_subsys ceph_subsys_rgw
namespace STS {
KeyringCache::KeyringCache(CephContext* cct, rgw::sal::Driver* driver,
std::chrono::milliseconds interval)
: cct(cct), driver(driver), interval(interval) {}
KeyringCache::~KeyringCache() { stop(); }
int KeyringCache::fetch(const std::string& key, ceph::bufferlist* bl)
{
return driver->get_config_key_val(key, bl);
}
void KeyringCache::refresh_keyring()
{
bufferlist bl;
int ret = fetch(std::string{rgw::sts::STS_KEYRING_CONFIG_KEY}, &bl);
if (ret == -ENOENT) {
// the keyring is gone; drop it so we stop sealing and verifying with it
if (snapshot.load()) {
ldout(cct, 1) << "STS keyring " << rgw::sts::STS_KEYRING_CONFIG_KEY
<< " was removed; revoking the cached keyring" << dendl;
}
snapshot.store(nullptr);
keyring_invalid = false;
return;
}
if (ret < 0) {
// mon error, keep the last keyring
ldout(cct, 5) << "WARNING: failed to refresh STS keyring: "
<< cpp_strerror(-ret) << dendl;
return;
}
std::string config = bl.to_str();
bl.zero();
auto keyring = std::make_shared<rgw::sts::StsKeyring>();
std::string err;
ret = rgw::sts::StsKeyring::parse(config, *keyring, err);
ceph_memzero_s(config.data(), config.size(), config.size());
if (ret < 0) {
if (!keyring_invalid) {
ldout(cct, 0) << "ERROR: STS keyring is invalid, keeping the previous"
" keyring: " << err << dendl;
keyring_invalid = true;
}
return;
}
keyring_invalid = false;
snapshot.store(KeyringSnapshot{std::move(keyring)});
}
void KeyringCache::refresh_legacy()
{
bufferlist bl;
int ret = fetch(std::string{rgw::sts::STS_LEGACY_KEY_CONFIG_KEY}, &bl);
if (ret == -ENOENT) {
if (legacy_snapshot.load()) {
ldout(cct, 1) << "legacy STS key " << rgw::sts::STS_LEGACY_KEY_CONFIG_KEY
<< " was removed; revoking the cached key" << dendl;
}
legacy_snapshot.store(nullptr);
legacy_conflict = false;
return;
}
if (ret < 0) {
// mon error, keep the last key
ldout(cct, 5) << "WARNING: failed to refresh the legacy STS key: "
<< cpp_strerror(-ret) << dendl;
return;
}
std::string key = bl.to_str();
bl.zero();
rgw::sts::trim_legacy_key(key);
if (key.empty()) {
// an empty value revokes the key just like removing it
legacy_snapshot.store(nullptr);
legacy_conflict = false;
return;
}
const std::string conf_key = cct->_conf.get_val<std::string>("rgw_sts_key");
if (!conf_key.empty() && conf_key != key) {
if (!legacy_conflict) {
ldout(cct, 0) << "WARNING: rgw_sts_key is set and differs from the"
" stored legacy STS key; this daemon seals and verifies legacy"
" tokens with rgw_sts_key" << dendl;
legacy_conflict = true;
}
} else {
legacy_conflict = false;
}
std::shared_ptr<std::string> next{
new std::string(std::move(key)),
[](std::string* p) {
ceph_memzero_s(p->data(), p->size(), p->size());
delete p;
}};
legacy_snapshot.store(LegacyKeySnapshot{std::move(next)});
}
void KeyringCache::refresh()
{
refresh_keyring();
refresh_legacy();
}
void KeyringCache::run(std::stop_token stop)
{
ceph_pthread_setname("sts-keyring");
std::mutex mutex;
std::condition_variable cond;
std::stop_callback on_stop(stop, [&cond]() { cond.notify_all(); });
while (!stop.stop_requested()) {
// refresh first so the keys load without blocking daemon startup
refresh();
std::unique_lock lock(mutex);
if (cond.wait_for(lock, interval,
[&stop] { return stop.stop_requested(); })) {
break;
}
}
}
void KeyringCache::start()
{
if (!thread.joinable()) {
thread = std::jthread([this](std::stop_token stop) { run(std::move(stop)); });
}
}
void KeyringCache::stop()
{
if (thread.joinable()) {
thread.request_stop();
thread.join();
}
}
void KeyringCache::pause()
{
stop();
}
void KeyringCache::resume(rgw::sal::Driver* new_driver)
{
driver = new_driver;
start();
}
} // namespace STS

View File

@ -0,0 +1,80 @@
// -*- mode:C++; tab-width:8; c-basic-offset:2; indent-tabs-mode:nil -*-
// vim: ts=8 sw=2 sts=2 expandtab ft=cpp
#pragma once
#include <atomic>
#include <chrono>
#include <memory>
#include <thread>
#include "include/buffer_fwd.h"
#include "rgw_realm_reloader.h"
#include "rgw_sts_keyring.h"
class CephContext;
namespace rgw::sal { class Driver; }
namespace STS {
using KeyringSnapshot = std::shared_ptr<const rgw::sts::StsKeyring>;
using LegacyKeySnapshot = std::shared_ptr<const std::string>;
/*
* Caches the token-sealing keyring and the stored legacy key from the mon
* config-key store. A background thread refreshes the snapshots every interval
* so requests just read them and never talk to the mon; the blocking reads
* stay off the shared executor.
*/
class KeyringCache : public RGWRealmReloader::Pauser {
CephContext* const cct;
rgw::sal::Driver* driver;
const std::chrono::milliseconds interval;
std::atomic<KeyringSnapshot> snapshot;
std::atomic<LegacyKeySnapshot> legacy_snapshot;
std::jthread thread;
// set while the stored keyring won't parse, to log the problem only once
bool keyring_invalid = false;
// set while rgw_sts_key overrides a different stored key, to log only once
bool legacy_conflict = false;
/*
* re-read a key and swap its snapshot. a missing key clears it; a mon
* error leaves the last one in place
*/
void refresh_keyring();
void refresh_legacy();
void run(std::stop_token stop);
protected:
// reads one config-key value from the mon; unit tests override this
virtual int fetch(const std::string& key, ceph::bufferlist* bl);
// one pass over both keys; the thread repeats it every interval
void refresh();
public:
KeyringCache(CephContext* cct, rgw::sal::Driver* driver,
std::chrono::milliseconds interval);
~KeyringCache() override;
KeyringCache(const KeyringCache&) = delete;
KeyringCache& operator=(const KeyringCache&) = delete;
// refresh the keyring on a background thread until stopped
void start();
void stop();
// realm reload stops the thread while the old driver is torn down, then
// rebinds to its replacement
void pause() override;
void resume(rgw::sal::Driver* new_driver) override;
// the current keyring, or nullptr when none is loaded
KeyringSnapshot get() const { return snapshot.load(); }
// the stored legacy key, or nullptr when none is loaded
LegacyKeySnapshot get_legacy() const { return legacy_snapshot.load(); }
};
} // namespace STS

View File

@ -27,5 +27,7 @@ public:
virtual ~RGWSI_ConfigKey() {}
virtual int get(const std::string& key, bool secure, bufferlist *result) = 0;
virtual int set(const std::string& key, const bufferlist& value, bool secure) = 0;
virtual int rm(const std::string& key) = 0;
};

View File

@ -24,8 +24,8 @@ void RGWSI_ConfigKey_RADOS::warn_if_insecure()
}
string s = ("rgw is configured to optionally allow insecure connections to "
"the monitors (auth_supported, ms_mon_client_mode), ssl "
"certificates stored at the monitor configuration could leak");
"the monitors (auth_supported, ms_mon_client_mode), secrets "
"stored at the monitor configuration could leak");
rgw_clog_warn(rados, s);
@ -52,3 +52,33 @@ int RGWSI_ConfigKey_RADOS::get(const string& key, bool secure,
return 0;
}
int RGWSI_ConfigKey_RADOS::set(const string& key, const bufferlist& value,
bool secure)
{
if (secure) {
// the secret crosses the monitor connection during the write
warn_if_insecure();
}
// the value travels in the input buffer, not the command string
string cmd =
"{"
"\"prefix\": \"config-key set\", "
"\"key\": \"" + key + "\""
"}";
bufferlist inbl = value;
return rados->mon_command(std::move(cmd), std::move(inbl), nullptr, nullptr);
}
int RGWSI_ConfigKey_RADOS::rm(const string& key)
{
string cmd =
"{"
"\"prefix\": \"config-key rm\", "
"\"key\": \"" + key + "\""
"}";
return rados->mon_command(std::move(cmd), {}, nullptr, nullptr);
}

View File

@ -43,4 +43,6 @@ public:
virtual ~RGWSI_ConfigKey_RADOS() override;
int get(const std::string& key, bool secure, bufferlist *result) override;
int set(const std::string& key, const bufferlist& value, bool secure) override;
int rm(const std::string& key) override;
};

View File

@ -207,6 +207,11 @@
mfa remove delete MFA TOTP token
mfa check check MFA TOTP token
mfa resync re-sync MFA TOTP token
sts keyring init initialize the sts token-sealing keyring
sts keyring rotate prepend a new key to the sts keyring
sts keyring list list the key ids of the sts keyring
sts keyring rm remove a key from the sts keyring
sts keyring trim remove the oldest keys from the sts keyring
topic list list bucket notifications topics
topic get get a bucket notifications topic
topic rm remove a bucket notifications topic
@ -413,6 +418,11 @@
--totp-window the number of TOTP tokens that are checked before and after the current token when validating token
--totp-pin the valid value of a TOTP token at a certain time
STS keyring options:
--key-id sts keyring key id (40 hexadecimal characters)
--max-keys keep at most this many keys after sts keyring rotate/trim
--legacy operate on the stored legacy sts key instead of the keyring
Bucket notifications options:
--topic bucket notifications topic name
--notification-id bucket notifications id

View File

@ -283,6 +283,14 @@ add_executable(unittest_rgw_throttle test_rgw_throttle.cc)
add_ceph_unittest(unittest_rgw_throttle)
target_link_libraries(unittest_rgw_throttle ${rgw_libs} ${UNITTEST_LIBS})
add_executable(unittest_rgw_sts test_rgw_sts.cc)
add_ceph_unittest(unittest_rgw_sts)
target_link_libraries(unittest_rgw_sts ${rgw_libs} ${UNITTEST_LIBS})
add_executable(unittest_rgw_sts_keyring_cache test_rgw_sts_keyring_cache.cc)
add_ceph_unittest(unittest_rgw_sts_keyring_cache)
target_link_libraries(unittest_rgw_sts_keyring_cache ${rgw_libs} ${UNITTEST_LIBS})
if(WITH_RADOSGW_RADOS)
add_executable(unittest_rgw_iam_policy test_rgw_iam_policy.cc)
add_ceph_unittest(unittest_rgw_iam_policy)

View File

@ -0,0 +1,386 @@
// -*- mode:C++; tab-width:8; c-basic-offset:2; indent-tabs-mode:nil -*-
// vim: ts=8 sw=2 sts=2 expandtab ft=cpp
/*
* Ceph - scalable distributed file system
*
* This is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License version 2.1, as published by the Free Software
* Foundation. See file COPYING.
*
*/
#include <algorithm>
#include <cctype>
#include <cerrno>
#include <iomanip>
#include <sstream>
#include <string>
#include <string_view>
#include <vector>
#include "gtest/gtest.h"
#include "common/ceph_argparse.h"
#include "common/dout.h"
#include "global/global_init.h"
#include "include/buffer.h"
#include "rgw_b64.h"
#include "rgw_sts.h"
#include "rgw_sts_keyring.h"
namespace {
using rgw::sts::StsKeyring;
using rgw::sts::sts_aead_key;
// build a raw key (20-byte id, 32-byte key) distinct from the parsed test keys
sts_aead_key make_raw_key(size_t index)
{
sts_aead_key key;
key.id.assign(20, static_cast<char>(0x10 + index));
key.key.assign(32, static_cast<char>(0x20 + index));
return key;
}
std::string make_sts_key_id(size_t index)
{
std::ostringstream out;
out << std::hex << std::setfill('0') << std::setw(40) << index;
return out.str();
}
std::string make_sts_key(size_t index)
{
return rgw::to_base64(std::string(32, static_cast<char>(index)));
}
std::string make_sts_keyring(size_t count)
{
std::ostringstream out;
for (size_t i = 0; i < count; ++i) {
if (i > 0) {
out << ' ';
}
out << make_sts_key_id(i) << '=' << make_sts_key(i);
}
return out.str();
}
int parse(std::string_view value, std::string& error)
{
StsKeyring keyring;
return StsKeyring::parse(value, keyring, error);
}
} // anonymous namespace
TEST(sts_keyring, accepts_valid_keyrings)
{
std::string error;
EXPECT_EQ(0, parse(make_sts_keyring(1), error));
EXPECT_EQ(0, parse(make_sts_keyring(16), error));
auto multiline_keyring = make_sts_keyring(2);
multiline_keyring.replace(multiline_keyring.find(' '), 1, "\n");
EXPECT_EQ(0, parse(multiline_keyring, error));
}
TEST(sts_keyring, rejects_empty_keyrings)
{
std::string error;
EXPECT_EQ(-EINVAL, parse("", error));
EXPECT_EQ(-EINVAL, parse(" \n\t", error));
}
TEST(sts_keyring, rejects_malformed_entries)
{
const auto id = make_sts_key_id(0);
const auto key = make_sts_key(0);
const std::vector<std::string> invalid = {
id,
id + "=",
id.substr(1) + '=' + key,
std::string(40, 'g') + '=' + key,
id + "=!!!!",
};
for (const auto& value : invalid) {
std::string error;
EXPECT_EQ(-EINVAL, parse(value, error)) << value;
}
std::string error;
EXPECT_EQ(-EINVAL, parse(id + "==", error));
EXPECT_NE(std::string::npos, error.find("does not decode to 32 bytes"));
}
TEST(sts_keyring, requires_canonical_base64)
{
const auto id = make_sts_key_id(0);
const auto key = make_sts_key(0);
auto noncanonical_pad_bits = key;
noncanonical_pad_bits[noncanonical_pad_bits.size() - 2] = 'B';
const std::vector<std::string> invalid = {
key.substr(0, key.size() - 1),
key + '=',
noncanonical_pad_bits,
rgw::to_base64(std::string(31, '\0')),
rgw::to_base64(std::string(33, '\0')),
};
for (const auto& encoded : invalid) {
std::string error;
EXPECT_EQ(-EINVAL, parse(id + '=' + encoded, error)) << encoded;
}
}
TEST(sts_keyring, rejects_duplicate_ids_and_keys)
{
const std::string mixed_case_id =
"0123456789abcdef0123456789abcdef01234567";
auto uppercase_id = mixed_case_id;
std::transform(uppercase_id.begin(), uppercase_id.end(),
uppercase_id.begin(),
[](unsigned char c) { return std::toupper(c); });
const auto key0 = make_sts_key(0);
const auto key1 = make_sts_key(1);
std::string error;
EXPECT_EQ(-EINVAL,
parse(mixed_case_id + '=' + key0 + ' ' +
uppercase_id + '=' + key1,
error));
EXPECT_EQ(-EINVAL,
parse(make_sts_key_id(0) + '=' + key0 + ' ' +
make_sts_key_id(1) + '=' + key0,
error));
}
TEST(sts_keyring, limits_key_count)
{
std::string error;
EXPECT_EQ(0, parse(make_sts_keyring(16), error));
EXPECT_EQ(-EINVAL, parse(make_sts_keyring(17), error));
EXPECT_NE(std::string::npos, error.find("at most 16"));
}
TEST(sts_keyring, format_round_trips)
{
StsKeyring keyring;
std::string error;
ASSERT_EQ(0, StsKeyring::parse(make_sts_keyring(3), keyring, error));
StsKeyring reparsed;
ASSERT_EQ(0, StsKeyring::parse(keyring.format(), reparsed, error));
ASSERT_EQ(keyring.size(), reparsed.size());
for (size_t i = 0; i < keyring.size(); ++i) {
EXPECT_EQ(keyring.entries()[i].id, reparsed.entries()[i].id);
EXPECT_EQ(keyring.entries()[i].key, reparsed.entries()[i].key);
}
}
TEST(sts_keyring, errors_do_not_echo_secrets)
{
const std::string secret = "not-a-valid-secret-key";
std::string error;
EXPECT_EQ(-EINVAL, parse(make_sts_key_id(0) + '=' + secret, error));
EXPECT_EQ(std::string::npos, error.find(secret));
}
TEST(sts_keyring, sealing_key_is_the_first_entry)
{
StsKeyring keyring;
std::string error;
ASSERT_EQ(0, StsKeyring::parse(make_sts_keyring(3), keyring, error));
EXPECT_EQ(&keyring.sealing_key(), &keyring.entries().front());
}
TEST(sts_keyring, find_locates_keys_by_id)
{
StsKeyring keyring;
std::string error;
ASSERT_EQ(0, StsKeyring::parse(make_sts_keyring(3), keyring, error));
const std::string id1 = keyring.entries()[1].id;
ASSERT_NE(nullptr, keyring.find(id1));
EXPECT_EQ(id1, keyring.find(id1)->id);
EXPECT_EQ(nullptr, keyring.find(std::string(20, '\xff')));
}
TEST(sts_keyring, prepend_adds_a_sealing_key)
{
StsKeyring keyring;
std::string error;
ASSERT_EQ(0, StsKeyring::parse(make_sts_keyring(2), keyring, error));
const std::string old_seal = keyring.sealing_key().id;
ASSERT_EQ(0, keyring.prepend(make_raw_key(9), error));
EXPECT_EQ(3u, keyring.size());
EXPECT_NE(old_seal, keyring.sealing_key().id);
EXPECT_EQ(-EINVAL, keyring.prepend(make_raw_key(9), error));
}
TEST(sts_keyring, prepend_rejects_a_full_keyring)
{
StsKeyring keyring;
std::string error;
ASSERT_EQ(0, StsKeyring::parse(make_sts_keyring(16), keyring, error));
EXPECT_EQ(-EINVAL, keyring.prepend(make_raw_key(99), error));
EXPECT_NE(std::string::npos, error.find("more than 16"));
}
TEST(sts_keyring, remove_drops_a_key)
{
StsKeyring keyring;
std::string error;
ASSERT_EQ(0, StsKeyring::parse(make_sts_keyring(3), keyring, error));
const std::string id1 = keyring.entries()[1].id;
ASSERT_EQ(0, keyring.remove(id1, error));
EXPECT_EQ(2u, keyring.size());
EXPECT_EQ(nullptr, keyring.find(id1));
EXPECT_EQ(-ENOENT, keyring.remove(std::string(20, '\xff'), error));
}
TEST(sts_keyring, remove_refuses_the_only_key)
{
StsKeyring keyring;
std::string error;
ASSERT_EQ(0, StsKeyring::parse(make_sts_keyring(1), keyring, error));
EXPECT_EQ(-EINVAL, keyring.remove(keyring.sealing_key().id, error));
EXPECT_EQ(1u, keyring.size());
}
TEST(sts_keyring, trim_drops_the_oldest_keeping_the_sealing_key)
{
StsKeyring keyring;
std::string error;
ASSERT_EQ(0, StsKeyring::parse(make_sts_keyring(5), keyring, error));
const std::string seal = keyring.sealing_key().id;
const auto removed = keyring.trim(2);
EXPECT_EQ(3u, removed.size());
EXPECT_EQ(2u, keyring.size());
EXPECT_EQ(seal, keyring.sealing_key().id);
}
TEST(sts_seal, round_trips)
{
StsKeyring keyring;
std::string error;
ASSERT_EQ(0, StsKeyring::parse(make_sts_keyring(3), keyring, error));
const NoDoutPrefix dpp(g_ceph_context, ceph_subsys_rgw);
const std::string body = "a session token body";
bufferlist plaintext;
plaintext.append(body);
std::string token;
ASSERT_EQ(0, STS::seal_session_token(&dpp, g_ceph_context,
keyring.sealing_key(), plaintext, token));
EXPECT_EQ(0u, token.rfind("v2.", 0));
bufferlist opened;
ASSERT_EQ(0, STS::unseal_session_token(&dpp, keyring, token, opened));
EXPECT_EQ(body, opened.to_str());
}
TEST(sts_seal, verifies_tokens_sealed_under_an_older_key)
{
StsKeyring keyring;
std::string error;
ASSERT_EQ(0, StsKeyring::parse(make_sts_keyring(1), keyring, error));
const NoDoutPrefix dpp(g_ceph_context, ceph_subsys_rgw);
const std::string body = "older key body";
bufferlist plaintext;
plaintext.append(body);
std::string token;
ASSERT_EQ(0, STS::seal_session_token(&dpp, g_ceph_context,
keyring.sealing_key(), plaintext, token));
// rotate: a new key seals, the old one stays only to verify
ASSERT_EQ(0, keyring.prepend(make_raw_key(5), error));
ASSERT_NE(keyring.sealing_key().id, keyring.entries().back().id);
bufferlist opened;
ASSERT_EQ(0, STS::unseal_session_token(&dpp, keyring, token, opened));
EXPECT_EQ(body, opened.to_str());
}
TEST(sts_seal, rejects_a_tampered_tag)
{
StsKeyring keyring;
std::string error;
ASSERT_EQ(0, StsKeyring::parse(make_sts_keyring(1), keyring, error));
const NoDoutPrefix dpp(g_ceph_context, ceph_subsys_rgw);
bufferlist plaintext;
plaintext.append(std::string("payload"));
std::string token;
ASSERT_EQ(0, STS::seal_session_token(&dpp, g_ceph_context,
keyring.sealing_key(), plaintext, token));
std::string envelope = rgw::from_base64(token.substr(3));
envelope[envelope.size() - 1] ^= 0x01;
const std::string tampered = "v2." + rgw::to_base64(envelope);
bufferlist opened;
EXPECT_EQ(-EPERM, STS::unseal_session_token(&dpp, keyring, tampered, opened));
}
TEST(sts_seal, rejects_an_unknown_key_id)
{
StsKeyring sealer;
StsKeyring other;
std::string error;
ASSERT_EQ(0, StsKeyring::parse(make_sts_keyring(1), sealer, error));
ASSERT_EQ(0, other.prepend(make_raw_key(7), error));
const NoDoutPrefix dpp(g_ceph_context, ceph_subsys_rgw);
bufferlist plaintext;
plaintext.append(std::string("payload"));
std::string token;
ASSERT_EQ(0, STS::seal_session_token(&dpp, g_ceph_context,
sealer.sealing_key(), plaintext, token));
bufferlist opened;
EXPECT_EQ(-EPERM, STS::unseal_session_token(&dpp, other, token, opened));
}
TEST(sts_seal, rejects_a_truncated_envelope)
{
StsKeyring keyring;
std::string error;
ASSERT_EQ(0, StsKeyring::parse(make_sts_keyring(1), keyring, error));
const NoDoutPrefix dpp(g_ceph_context, ceph_subsys_rgw);
bufferlist plaintext;
plaintext.append(std::string("payload"));
std::string token;
ASSERT_EQ(0, STS::seal_session_token(&dpp, g_ceph_context,
keyring.sealing_key(), plaintext, token));
std::string envelope = rgw::from_base64(token.substr(3));
envelope.resize(envelope.size() / 2);
const std::string truncated = "v2." + rgw::to_base64(envelope);
bufferlist opened;
EXPECT_EQ(-EINVAL, STS::unseal_session_token(&dpp, keyring, truncated, opened));
}
int main(int argc, char** argv)
{
auto args = argv_to_vec(argc, argv);
auto cct = global_init(NULL, args, CEPH_ENTITY_TYPE_CLIENT,
CODE_ENVIRONMENT_UTILITY,
CINIT_FLAG_NO_DEFAULT_CONFIG_FILE);
common_init_finish(g_ceph_context);
::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}

View File

@ -0,0 +1,160 @@
// -*- mode:C++; tab-width:8; c-basic-offset:2; indent-tabs-mode:nil -*-
// vim: ts=8 sw=2 sts=2 expandtab ft=cpp
/*
* Ceph - scalable distributed file system
*
* This is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License version 2.1, as published by the Free Software
* Foundation. See file COPYING.
*
*/
#include <cerrno>
#include <chrono>
#include <map>
#include <string>
#include "gtest/gtest.h"
#include "common/ceph_argparse.h"
#include "global/global_init.h"
#include "include/buffer.h"
#include "rgw_b64.h"
#include "rgw_sts_keyring.h"
#include "rgw_sts_keyring_cache.h"
namespace {
const std::string keyring_key{rgw::sts::STS_KEYRING_CONFIG_KEY};
const std::string legacy_key{rgw::sts::STS_LEGACY_KEY_CONFIG_KEY};
// one <hex-id>=<base64-key> keyring entry, distinct per index
std::string make_entry(size_t index)
{
std::string id(40, '0');
id.back() = static_cast<char>('0' + index);
return id + '=' + rgw::to_base64(std::string(32, static_cast<char>(index)));
}
// serves canned config-key responses
class FakeKeyringCache : public STS::KeyringCache {
public:
explicit FakeKeyringCache(CephContext* cct)
: KeyringCache(cct, nullptr, std::chrono::seconds(60)) {}
// join before the override goes away; ~KeyringCache is too late
~FakeKeyringCache() override { stop(); }
using KeyringCache::refresh;
void set_response(const std::string& key, int ret, const std::string& value)
{
responses[key] = {ret, value};
}
private:
int fetch(const std::string& key, ceph::bufferlist* bl) override
{
const auto& response = responses[key];
if (response.ret == 0) {
bl->append(response.value);
}
return response.ret;
}
struct Response {
int ret = -ENOENT;
std::string value;
};
std::map<std::string, Response> responses;
};
class sts_keyring_cache : public ::testing::Test {
protected:
FakeKeyringCache cache{g_ceph_context};
};
} // anonymous namespace
TEST_F(sts_keyring_cache, loads_and_serves)
{
cache.set_response(keyring_key, 0, make_entry(1) + '\n' + make_entry(2));
cache.set_response(legacy_key, 0, "0123456789abcdef\n");
cache.refresh();
const auto keyring = cache.get();
ASSERT_TRUE(keyring);
EXPECT_EQ(2u, keyring->size());
// the stored value is served with its trailing whitespace trimmed
const auto legacy = cache.get_legacy();
ASSERT_TRUE(legacy);
EXPECT_EQ("0123456789abcdef", *legacy);
}
TEST_F(sts_keyring_cache, removal_revokes)
{
cache.set_response(keyring_key, 0, make_entry(1));
cache.set_response(legacy_key, 0, "0123456789abcdef");
cache.refresh();
ASSERT_TRUE(cache.get());
ASSERT_TRUE(cache.get_legacy());
cache.set_response(keyring_key, -ENOENT, "");
cache.set_response(legacy_key, -ENOENT, "");
cache.refresh();
EXPECT_FALSE(cache.get());
EXPECT_FALSE(cache.get_legacy());
}
TEST_F(sts_keyring_cache, errors_keep_the_last_snapshot)
{
cache.set_response(keyring_key, 0, make_entry(1));
cache.set_response(legacy_key, 0, "0123456789abcdef");
cache.refresh();
// an unparseable keyring and a mon error both leave the snapshots alone
cache.set_response(keyring_key, 0, "not a keyring");
cache.set_response(legacy_key, -EIO, "");
cache.refresh();
const auto keyring = cache.get();
ASSERT_TRUE(keyring);
EXPECT_EQ(1u, keyring->size());
EXPECT_TRUE(cache.get_legacy());
}
TEST_F(sts_keyring_cache, empty_legacy_value_revokes)
{
cache.set_response(legacy_key, 0, "0123456789abcdef");
cache.refresh();
ASSERT_TRUE(cache.get_legacy());
cache.set_response(legacy_key, 0, " \t\r\n");
cache.refresh();
EXPECT_FALSE(cache.get_legacy());
}
TEST_F(sts_keyring_cache, stop_keeps_the_snapshots)
{
cache.set_response(keyring_key, 0, make_entry(1));
cache.refresh();
cache.start();
cache.stop();
EXPECT_TRUE(cache.get());
EXPECT_FALSE(cache.get_legacy());
}
int main(int argc, char** argv)
{
auto args = argv_to_vec(argc, argv);
auto cct = global_init(NULL, args, CEPH_ENTITY_TYPE_CLIENT,
CODE_ENVIRONMENT_UTILITY,
CINIT_FLAG_NO_DEFAULT_CONFIG_FILE);
common_init_finish(g_ceph_context);
::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}