From 3a5b6eb11d02fe1ffd35c9eacb60e6254ba3d65b Mon Sep 17 00:00:00 2001 From: Zhengchao An Date: Mon, 3 Aug 2026 01:25:49 +0800 Subject: [PATCH] test(kms): verify AppRole against live Vault (#5650) * test(kms): add ignored Vault AppRole live harness * test(kms): tighten Vault AppRole live contract --- crates/kms/tests/vault_approle_live.rs | 117 +++++++++++++++ scripts/test/vault_approle_kms_live.sh | 196 +++++++++++++++++++++++++ 2 files changed, 313 insertions(+) create mode 100644 crates/kms/tests/vault_approle_live.rs create mode 100755 scripts/test/vault_approle_kms_live.sh diff --git a/crates/kms/tests/vault_approle_live.rs b/crates/kms/tests/vault_approle_live.rs new file mode 100644 index 000000000..441ce8107 --- /dev/null +++ b/crates/kms/tests/vault_approle_live.rs @@ -0,0 +1,117 @@ +// Copyright 2024 RustFS Team +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Ignored live-Vault checks for the AppRole authentication path. +//! +//! `scripts/test/vault_approle_kms_live.sh` starts an ephemeral Vault, creates +//! a narrowly scoped AppRole, and runs each test with only the generated +//! role_id and secret_id. The tests then build the KMS configuration from the +//! same environment variables used by RustFS and exercise the real KV2 and +//! Transit backend calls with the AppRole-issued token. + +use rustfs_kms::backends::KmsBackend as KmsBackendTrait; +use rustfs_kms::backends::vault::VaultKmsBackend; +use rustfs_kms::backends::vault_transit::VaultTransitKmsBackend; +use rustfs_kms::{ + BackendConfig, CreateKeyRequest, DEFAULT_VAULT_TRANSIT_METADATA_KEY_PREFIX, DEFAULT_VAULT_TRANSIT_METADATA_KV_MOUNT, + DecryptRequest, GenerateDataKeyRequest, KeySpec, KeyUsage, KmsBackend, KmsConfig, ListKeysRequest, VaultAuthMethod, +}; +use std::collections::HashMap; + +fn assert_approle_config(config: &KmsConfig, expected_backend: KmsBackend) { + assert_eq!(config.backend, expected_backend); + let auth_method = match &config.backend_config { + BackendConfig::VaultKv2(vault) => &vault.auth_method, + BackendConfig::VaultTransit(vault) => &vault.auth_method, + _ => panic!("expected Vault configuration"), + }; + assert!( + matches!(auth_method, VaultAuthMethod::AppRole { .. }), + "live check must use Vault AppRole auth" + ); +} + +async fn exercise_backend(backend: &B, key_prefix: &str) -> rustfs_kms::Result<()> { + let key_id = format!("{key_prefix}-{}", uuid::Uuid::new_v4()); + let created = backend + .create_key(CreateKeyRequest { + key_name: Some(key_id.clone()), + key_usage: KeyUsage::EncryptDecrypt, + ..Default::default() + }) + .await?; + assert_eq!(created.key_id, key_id); + + let described = backend + .describe_key(rustfs_kms::DescribeKeyRequest { key_id: key_id.clone() }) + .await?; + assert_eq!(described.key_metadata.key_id, key_id); + + let listed = backend + .list_keys(ListKeysRequest { + limit: Some(100), + ..Default::default() + }) + .await?; + assert!( + listed.keys.iter().any(|key| key.key_id == key_id), + "created key must be visible in the backend listing" + ); + + let context = HashMap::from([("live".to_string(), "approle".to_string())]); + let generated = backend + .generate_data_key(GenerateDataKeyRequest { + key_id: key_id.clone(), + key_spec: KeySpec::Aes256, + encryption_context: context.clone(), + }) + .await?; + assert_eq!(generated.key_id, key_id); + assert_eq!(generated.plaintext_key.len(), 32, "AES-256 must return a 32-byte data key"); + + let unwrapped = backend + .decrypt(DecryptRequest { + ciphertext: generated.ciphertext_blob, + encryption_context: context, + grant_tokens: Vec::new(), + }) + .await?; + assert_eq!(unwrapped.plaintext, generated.plaintext_key); + Ok(()) +} + +#[tokio::test] +#[ignore = "requires a real Vault AppRole; run scripts/test/vault_approle_kms_live.sh"] +async fn vault_kv2_approle_auth_live() -> rustfs_kms::Result<()> { + let config = KmsConfig::from_env()?; + assert_approle_config(&config, KmsBackend::VaultKv2); + let backend = VaultKmsBackend::new(config).await?; + exercise_backend(&backend, "rustfs-approle-kv2").await +} + +#[tokio::test] +#[ignore = "requires a real Vault AppRole; run scripts/test/vault_approle_kms_live.sh"] +async fn vault_transit_approle_auth_live() -> rustfs_kms::Result<()> { + let config = KmsConfig::from_env()?; + assert_approle_config(&config, KmsBackend::VaultTransit); + let transit = match &config.backend_config { + BackendConfig::VaultTransit(vault) => vault, + _ => panic!("expected Vault Transit configuration"), + }; + assert_eq!(transit.metadata_kv_mount, DEFAULT_VAULT_TRANSIT_METADATA_KV_MOUNT); + assert_eq!(transit.metadata_key_prefix, DEFAULT_VAULT_TRANSIT_METADATA_KEY_PREFIX); + + let backend = VaultTransitKmsBackend::new(config).await?; + exercise_backend(&backend, "rustfs-approle-transit").await +} diff --git a/scripts/test/vault_approle_kms_live.sh b/scripts/test/vault_approle_kms_live.sh new file mode 100755 index 000000000..398daffd4 --- /dev/null +++ b/scripts/test/vault_approle_kms_live.sh @@ -0,0 +1,196 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Start an ephemeral Vault, issue a least-privilege AppRole, and run the two +# ignored RustFS KMS checks without ever exposing the generated credentials. + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)" +VAULT_BIN="${RUSTFS_TEST_VAULT_BIN:-vault}" + +if ! command -v "$VAULT_BIN" >/dev/null 2>&1; then + echo "Vault binary not found; set RUSTFS_TEST_VAULT_BIN or install vault" >&2 + exit 1 +fi +if ! command -v python3 >/dev/null 2>&1; then + echo "python3 is required to allocate an ephemeral loopback port" >&2 + exit 1 +fi +TMP_DIR="$(mktemp -d -t rustfs-vault-approle-live.XXXXXX)" +VAULT_PID="" + +cleanup() { + if [[ -n "$VAULT_PID" ]]; then + kill "$VAULT_PID" 2>/dev/null || true + wait "$VAULT_PID" 2>/dev/null || true + fi + rm -rf "$TMP_DIR" +} +trap cleanup EXIT + +VAULT_PORT="$(python3 - <<'PY' +import socket + +with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: + sock.bind(("127.0.0.1", 0)) + print(sock.getsockname()[1]) +PY +)" +VAULT_ADDR="http://127.0.0.1:${VAULT_PORT}" +ROOT_TOKEN="rustfs-approle-live-root-$$-${RANDOM}" +INHERITED_NO_PROXY="${NO_PROXY:-}" +INHERITED_NO_PROXY_LOWER="${no_proxy:-}" +if [[ -n "$INHERITED_NO_PROXY" ]]; then + LOCAL_NO_PROXY="${INHERITED_NO_PROXY},127.0.0.1,localhost" +else + LOCAL_NO_PROXY="127.0.0.1,localhost" +fi +if [[ -n "$INHERITED_NO_PROXY_LOWER" ]]; then + LOCAL_NO_PROXY_LOWER="${INHERITED_NO_PROXY_LOWER},127.0.0.1,localhost" +else + LOCAL_NO_PROXY_LOWER="$LOCAL_NO_PROXY" +fi + +"$VAULT_BIN" server \ + -dev \ + -dev-root-token-id "$ROOT_TOKEN" \ + -dev-listen-address "127.0.0.1:${VAULT_PORT}" \ + >"${TMP_DIR}/vault.log" 2>&1 & +VAULT_PID=$! + +vault_cli() { + NO_PROXY="$LOCAL_NO_PROXY" no_proxy="$LOCAL_NO_PROXY_LOWER" \ + VAULT_ADDR="$VAULT_ADDR" VAULT_TOKEN="$ROOT_TOKEN" "$VAULT_BIN" "$@" +} + +ready=0 +for _ in $(seq 1 120); do + if vault_cli status >/dev/null 2>&1; then + ready=1 + break + fi + sleep 0.25 +done +if [[ "$ready" != 1 ]]; then + echo "Vault did not become ready" >&2 + exit 1 +fi + +vault_cli secrets enable -path=transit transit >/dev/null +vault_cli auth enable approle >/dev/null + +POLICY_NAME="rustfs-kms-live-$$" +ROLE_NAME="rustfs-kms-live-$$" +POLICY_FILE="${TMP_DIR}/policy.hcl" +cat >"$POLICY_FILE" <<'EOF' +# KV2 master-key records. +path "secret/data/rustfs/kms/keys/*" { + capabilities = ["create", "read", "update"] +} +path "secret/metadata/rustfs/kms/keys/*" { + capabilities = ["list", "read", "delete"] +} +path "secret/metadata/rustfs/kms/keys" { + capabilities = ["list"] +} + +# Transit metadata uses the default RustFS KV2 path. +path "secret/data/rustfs/kms/transit-metadata/*" { + capabilities = ["create", "read", "update"] +} +path "secret/metadata/rustfs/kms/transit-metadata/*" { + capabilities = ["list", "read", "delete"] +} +path "secret/metadata/rustfs/kms/transit-metadata" { + capabilities = ["list"] +} + +# Transit key lifecycle and data-path operations. +path "transit/keys" { + capabilities = ["list"] +} +path "transit/keys/*" { + capabilities = ["create", "read", "update"] +} +path "transit/encrypt/*" { + capabilities = ["update"] +} +path "transit/decrypt/*" { + capabilities = ["update"] +} +EOF + +vault_cli policy write "$POLICY_NAME" "$POLICY_FILE" >/dev/null +vault_cli write "auth/approle/role/${ROLE_NAME}" \ + token_policies="$POLICY_NAME" \ + token_ttl=10m \ + token_max_ttl=20m \ + secret_id_ttl=30m \ + secret_id_num_uses=0 \ + >/dev/null + +ROLE_ID="$(vault_cli read -field=role_id "auth/approle/role/${ROLE_NAME}/role-id")" +SECRET_ID="$(vault_cli write -f -field=secret_id "auth/approle/role/${ROLE_NAME}/secret-id")" + +run_live_test() { + local backend="$1" + local test_name="$2" + + local -a backend_env=( + -u RUSTFS_KMS_BACKEND + -u RUSTFS_KMS_VAULT_ADDRESS + -u RUSTFS_KMS_VAULT_NAMESPACE + -u RUSTFS_KMS_VAULT_SKIP_TLS_VERIFY + -u RUSTFS_KMS_VAULT_MOUNT_PATH + -u RUSTFS_KMS_VAULT_KV_MOUNT + -u RUSTFS_KMS_VAULT_KEY_PREFIX + -u RUSTFS_KMS_VAULT_TRANSIT_METADATA_KV_MOUNT + -u RUSTFS_KMS_VAULT_TRANSIT_METADATA_PREFIX + -u RUSTFS_KMS_VAULT_TOKEN + -u RUSTFS_KMS_VAULT_TOKEN_FILE + -u RUSTFS_KMS_VAULT_APPROLE_ROLE_ID + -u RUSTFS_KMS_VAULT_APPROLE_SECRET_ID + -u RUSTFS_KMS_VAULT_APPROLE_SECRET_ID_FILE + -u RUSTFS_KMS_VAULT_APPROLE_MOUNT + -u RUSTFS_KMS_ALLOW_INSECURE_DEV_DEFAULTS + -u RUSTFS_KMS_TIMEOUT_SECS + -u RUSTFS_KMS_RETRY_ATTEMPTS + -u RUSTFS_KMS_ENABLE_CACHE + -u NO_PROXY + -u no_proxy + RUSTFS_KMS_BACKEND="$backend" + RUSTFS_KMS_VAULT_ADDRESS="$VAULT_ADDR" + RUSTFS_KMS_VAULT_APPROLE_ROLE_ID="$ROLE_ID" + RUSTFS_KMS_VAULT_APPROLE_SECRET_ID="$SECRET_ID" + RUSTFS_KMS_VAULT_APPROLE_MOUNT=approle + RUSTFS_KMS_ALLOW_INSECURE_DEV_DEFAULTS=true + RUSTFS_KMS_TIMEOUT_SECS=10 + RUSTFS_KMS_RETRY_ATTEMPTS=1 + RUSTFS_KMS_ENABLE_CACHE=false + NO_PROXY="$LOCAL_NO_PROXY" + no_proxy="$LOCAL_NO_PROXY_LOWER" + ) + + if [[ "$backend" == vault ]]; then + backend_env+=( + RUSTFS_KMS_VAULT_KV_MOUNT=secret + RUSTFS_KMS_VAULT_KEY_PREFIX=rustfs/kms/keys + ) + else + backend_env+=( + RUSTFS_KMS_VAULT_MOUNT_PATH=transit + RUSTFS_KMS_VAULT_TRANSIT_METADATA_KV_MOUNT=secret + RUSTFS_KMS_VAULT_TRANSIT_METADATA_PREFIX=rustfs/kms/transit-metadata + ) + fi + + env \ + "${backend_env[@]}" \ + cargo test -p rustfs-kms --test vault_approle_live "$test_name" -- \ + --ignored --nocapture --test-threads=1 +} + +cd "$PROJECT_ROOT" +run_live_test vault vault_kv2_approle_auth_live +run_live_test vault-transit vault_transit_approle_auth_live +echo "Vault AppRole KV2 and Transit live checks passed"