Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions deploy/rustfs-operator/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,25 @@ Operator STS does not present a client certificate when calling the Tenant. Tena

When `operator.serviceMonitor.enabled=true`, the chart creates scrape targets for both the operator observability endpoint and the Console API `/metrics` endpoint.

### Tenant RPC Authentication

Use `spec.rpcSecret` to keep RustFS internode RPC authentication independent from
the administrator credentials in `spec.credsSecret`:

```yaml
spec:
credsSecret:
name: rustfs-admin-creds
rpcSecret:
name: rustfs-rpc-auth
key: rpc-secret
```

The operator maps the selected Secret key to `RUSTFS_RPC_SECRET` in every RustFS
Pod. Keep this value stable while rotating administrator credentials. If
`spec.rpcSecret` is omitted, the operator does not set `RUSTFS_RPC_SECRET` and
RustFS resolves the RPC secret from its own credential configuration.

### Tenant Provisioning

Tenants can declare RustFS canned policies, regular users, and buckets directly in `spec.policies`, `spec.users`, and `spec.buckets`. Provisioning starts only after the Tenant workload is ready, uses `spec.credsSecret` as the RustFS admin credential source, and reports progress under `status.provisioning`.
Expand Down
20 changes: 20 additions & 0 deletions deploy/rustfs-operator/crds/tenant-crd.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1410,6 +1410,26 @@ spec:
priorityClassName:
nullable: true
type: string
rpcSecret:
description: |-
Optional Secret key selector for RustFS internode RPC authentication.

When configured, the operator maps this key to `RUSTFS_RPC_SECRET` for every
RustFS Pod. Keep it stable while rotating `credsSecret`. When omitted, the
operator does not set `RUSTFS_RPC_SECRET`; RustFS resolves it from its own
credential configuration.
nullable: true
properties:
key:
minLength: 1
type: string
name:
minLength: 1
type: string
required:
- key
- name
type: object
scheduler:
nullable: true
type: string
Expand Down
20 changes: 20 additions & 0 deletions deploy/rustfs-operator/crds/tenant.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1410,6 +1410,26 @@ spec:
priorityClassName:
nullable: true
type: string
rpcSecret:
description: |-
Optional Secret key selector for RustFS internode RPC authentication.

When configured, the operator maps this key to `RUSTFS_RPC_SECRET` for every
RustFS Pod. Keep it stable while rotating `credsSecret`. When omitted, the
operator does not set `RUSTFS_RPC_SECRET`; RustFS resolves it from its own
credential configuration.
nullable: true
properties:
key:
minLength: 1
type: string
name:
minLength: 1
type: string
required:
- key
- name
type: object
scheduler:
nullable: true
type: string
Expand Down
14 changes: 14 additions & 0 deletions src/types/v1alpha1.rs
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,20 @@ mod tenant_provisioning_crd_tests {
let spec = &schema["properties"]["spec"];
let status = &schema["properties"]["status"];

assert_eq!(spec["properties"]["rpcSecret"]["type"], json!("object"));
assert_eq!(
spec["properties"]["rpcSecret"]["properties"]["name"]["minLength"],
json!(1)
);
assert_eq!(
spec["properties"]["rpcSecret"]["properties"]["key"]["minLength"],
json!(1)
);
assert_eq!(
spec["properties"]["rpcSecret"]["required"],
json!(["key", "name"])
);

assert_eq!(spec["properties"]["policies"]["type"], json!("array"));
assert_eq!(spec["properties"]["users"]["type"], json!("array"));
assert_eq!(spec["properties"]["buckets"]["type"], json!("array"));
Expand Down
20 changes: 20 additions & 0 deletions src/types/v1alpha1/tenant.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,17 @@ pub(crate) const MAX_TENANT_POLICIES: u32 = 256;
pub(crate) const MAX_TENANT_USERS: u32 = 256;
pub(crate) const MAX_TENANT_BUCKETS: u32 = 1024;

/// Reference to one key in a namespaced Kubernetes Secret.
#[derive(Deserialize, Serialize, Clone, Debug, KubeSchema, Default, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct RpcSecretRef {
#[schemars(length(min = 1))]
pub name: String,

#[schemars(length(min = 1))]
pub key: String,
}

#[derive(CustomResource, Deserialize, Serialize, Clone, Debug, KubeSchema, Default)]
#[kube(
group = "rustfs.com",
Expand Down Expand Up @@ -169,6 +180,15 @@ pub struct TenantSpec {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub creds_secret: Option<corev1::LocalObjectReference>,

/// Optional Secret key selector for RustFS internode RPC authentication.
///
/// When configured, the operator maps this key to `RUSTFS_RPC_SECRET` for every
/// RustFS Pod. Keep it stable while rotating `credsSecret`. When omitted, the
/// operator does not set `RUSTFS_RPC_SECRET`; RustFS resolves it from its own
/// credential configuration.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub rpc_secret: Option<RpcSecretRef>,

/// Canned policies that should be applied to the RustFS tenant.
#[schemars(
length(max = MAX_TENANT_POLICIES),
Expand Down
127 changes: 127 additions & 0 deletions src/types/v1alpha1/tenant/workloads.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ const LOCAL_KMS_KEY_DIR_ENV: &str = "RUSTFS_KMS_KEY_DIR";
const LOCAL_KMS_LOCAL_KEY_DIR_ENV: &str = "RUSTFS_KMS_LOCAL_KEY_DIR";
const LOCAL_KMS_MASTER_KEY_ENV: &str = "RUSTFS_KMS_LOCAL_MASTER_KEY";
const KMS_ALLOW_INSECURE_DEV_DEFAULTS_ENV: &str = "RUSTFS_KMS_ALLOW_INSECURE_DEV_DEFAULTS";
const RPC_SECRET_ENV: &str = "RUSTFS_RPC_SECRET";
const VOLUME_CLAIM_TEMPLATE_PREFIX: &str = "vol";
const DEFAULT_RUN_AS_USER: i64 = 10001;
const DEFAULT_RUN_AS_GROUP: i64 = 10001;
Expand Down Expand Up @@ -522,6 +523,24 @@ impl Tenant {
});
}

// Keep internode RPC authentication independent from admin credentials when
// the Tenant explicitly selects a dedicated Secret key. If omitted, RustFS
// retains ownership of RPC secret resolution.
if let Some(ref secret_ref) = self.spec.rpc_secret {
env_vars.push(corev1::EnvVar {
name: RPC_SECRET_ENV.to_owned(),
value_from: Some(corev1::EnvVarSource {
secret_key_ref: Some(corev1::SecretKeySelector {
name: secret_ref.name.clone(),
key: secret_ref.key.clone(),
optional: Some(false),
}),
..Default::default()
}),
..Default::default()
});
}

// Merge with user-provided environment variables.
// Preserve the legacy override behavior except for operator-managed runtime
// values that must stay aligned with rendered mounts, probes, status, and hash.
Expand All @@ -532,6 +551,9 @@ impl Tenant {
if is_kms_operator_managed_env_var(&user_env.name) {
continue;
}
if self.spec.rpc_secret.is_some() && user_env.name == RPC_SECRET_ENV {
continue;
}
// Remove any existing var with the same name to allow non-reserved overrides.
env_vars.retain(|e| e.name != user_env.name);
env_vars.push(user_env.clone());
Expand Down Expand Up @@ -1121,6 +1143,7 @@ mod tests {
EncryptionConfig, KmsBackendType, LocalKmsConfig, LocalKmsMasterKeySecretRef,
};
use crate::types::v1alpha1::logging::{LoggingConfig, LoggingMode};
use crate::types::v1alpha1::tenant::RpcSecretRef;
use crate::types::v1alpha1::tls::{SecretKeyReference, TlsPlan};
use k8s_openapi::api::core::v1 as corev1;

Expand Down Expand Up @@ -1233,6 +1256,86 @@ mod tests {
}));
}

#[test]
fn omitted_rpc_secret_leaves_rpc_auth_resolution_to_rustfs() {
let tenant = crate::tests::create_test_tenant(None, None);
let pool = &tenant.spec.pools[0];

let statefulset = tenant
.new_statefulset(pool)
.expect("Should create StatefulSet without an RPC Secret");
let container = &statefulset.spec.unwrap().template.spec.unwrap().containers[0];

assert!(
container
.env
.as_ref()
.is_none_or(|env| env.iter().all(|var| var.name != "RUSTFS_RPC_SECRET"))
);
}

#[test]
fn rpc_secret_maps_selected_secret_key_and_owns_the_env_var() {
let mut tenant = crate::tests::create_test_tenant(None, None);
tenant.spec.rpc_secret = Some(RpcSecretRef {
name: "tenant-rpc-auth".to_string(),
key: "rpc-secret".to_string(),
});
tenant.spec.env.push(corev1::EnvVar {
name: "RUSTFS_RPC_SECRET".to_string(),
value: Some("raw-override-must-not-win".to_string()),
..Default::default()
});
let pool = &tenant.spec.pools[0];

let statefulset = tenant
.new_statefulset(pool)
.expect("Should create StatefulSet with an RPC Secret");
let container = &statefulset.spec.unwrap().template.spec.unwrap().containers[0];
let rpc_env = container
.env
.as_ref()
.unwrap()
.iter()
.filter(|var| var.name == "RUSTFS_RPC_SECRET")
.collect::<Vec<_>>();

assert_eq!(rpc_env.len(), 1);
assert_eq!(rpc_env[0].value, None);
assert_eq!(
rpc_env[0]
.value_from
.as_ref()
.and_then(|source| source.secret_key_ref.as_ref()),
Some(&corev1::SecretKeySelector {
name: "tenant-rpc-auth".to_string(),
key: "rpc-secret".to_string(),
optional: Some(false),
})
);
}

#[test]
fn raw_rpc_secret_env_remains_supported_without_rpc_secret_ref() {
let mut tenant = crate::tests::create_test_tenant(None, None);
tenant.spec.env.push(corev1::EnvVar {
name: "RUSTFS_RPC_SECRET".to_string(),
value: Some("legacy-explicit-rpc-secret".to_string()),
..Default::default()
});
let pool = &tenant.spec.pools[0];

let statefulset = tenant
.new_statefulset(pool)
.expect("Should preserve the legacy raw RPC Secret environment variable");
let container = &statefulset.spec.unwrap().template.spec.unwrap().containers[0];

assert_eq!(
env_value(container, "RUSTFS_RPC_SECRET"),
Some("legacy-explicit-rpc-secret")
);
}

#[test]
fn cert_manager_tls_statefulset_maps_secret_to_rustfs_tls_files() {
let tenant = crate::tests::create_test_tenant(None, None);
Expand Down Expand Up @@ -2439,6 +2542,30 @@ mod tests {
);
}

#[test]
fn test_statefulset_rpc_secret_change_detected() {
let mut tenant = crate::tests::create_test_tenant(None, None);
tenant.spec.rpc_secret = Some(RpcSecretRef {
name: "tenant-rpc-auth".to_string(),
key: "rpc-secret".to_string(),
});
let pool = &tenant.spec.pools[0];
let statefulset = tenant
.new_statefulset(pool)
.expect("Should create StatefulSet");

tenant.spec.rpc_secret.as_mut().unwrap().name = "rotated-rpc-auth".to_string();

let needs_update = tenant
.statefulset_needs_update(&statefulset, pool)
.expect("Should check update need");

assert!(
needs_update,
"StatefulSet should need update when the RPC Secret reference changes"
);
}

// Test: StatefulSet diff detection - resources change
#[test]
fn test_statefulset_resources_change_detected() {
Expand Down
Loading