mirror of
https://github.com/ceph/ceph
synced 2026-08-02 07:03:18 +00:00
Merge 5b42700e2f into eed82023c5
This commit is contained in:
commit
a62e955ad7
@ -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.
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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.
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
7
qa/suites/rgw/sts/tasks/1-keyring.yaml
Normal file
7
qa/suites/rgw/sts/tasks/1-keyring.yaml
Normal 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
|
||||
5
qa/suites/rgw/sts/tasks/3-keyring-lifecycle.yaml
Normal file
5
qa/suites/rgw/sts/tasks/3-keyring-lifecycle.yaml
Normal file
@ -0,0 +1,5 @@
|
||||
tasks:
|
||||
- workunit:
|
||||
clients:
|
||||
client.0:
|
||||
- rgw/test_rgw_sts_keyring.sh
|
||||
1
qa/suites/rgw/sts/token-seal$/.qa
Symbolic link
1
qa/suites/rgw/sts/token-seal$/.qa
Symbolic link
@ -0,0 +1 @@
|
||||
../.qa/
|
||||
6
qa/suites/rgw/sts/token-seal$/aead.yaml
Normal file
6
qa/suites/rgw/sts/token-seal$/aead.yaml
Normal file
@ -0,0 +1,6 @@
|
||||
overrides:
|
||||
ceph:
|
||||
conf:
|
||||
client:
|
||||
rgw sts token format: aead
|
||||
rgw sts keyring refresh interval: 1
|
||||
0
qa/suites/rgw/sts/token-seal$/legacy.yaml
Normal file
0
qa/suites/rgw/sts/token-seal$/legacy.yaml
Normal 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',
|
||||
|
||||
97
qa/workunits/rgw/test_rgw_sts_keyring.sh
Executable file
97
qa/workunits/rgw/test_rgw_sts_keyring.sh
Executable 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
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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();
|
||||
|
||||
|
||||
@ -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()),
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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
263
src/rgw/rgw_sts_keyring.h
Normal 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
|
||||
165
src/rgw/rgw_sts_keyring_cache.cc
Normal file
165
src/rgw/rgw_sts_keyring_cache.cc
Normal 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
|
||||
80
src/rgw/rgw_sts_keyring_cache.h
Normal file
80
src/rgw/rgw_sts_keyring_cache.h
Normal 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
|
||||
@ -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;
|
||||
};
|
||||
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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;
|
||||
};
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
386
src/test/rgw/test_rgw_sts.cc
Normal file
386
src/test/rgw/test_rgw_sts.cc
Normal 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();
|
||||
}
|
||||
160
src/test/rgw/test_rgw_sts_keyring_cache.cc
Normal file
160
src/test/rgw/test_rgw_sts_keyring_cache.cc
Normal 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();
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user