Skip to content

Commit 59aabf7

Browse files
committed
fix(security): align credential validation with runtime
1 parent 1af2d17 commit 59aabf7

8 files changed

Lines changed: 88 additions & 28 deletions

File tree

deploy/rustfs-operator/crds/tenant-crd.yaml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,8 @@ spec:
174174
credsSecret:
175175
description: |-
176176
Optional reference to a Secret containing RustFS credentials.
177-
The Secret must contain 'accesskey' and 'secretkey' keys (both required, minimum 8 characters each).
177+
The Secret must contain 'accesskey' and 'secretkey' keys. Both values must be valid
178+
UTF-8 without NUL bytes and at least 8 UTF-8 bytes after trimming.
178179
If not specified, credentials can be provided via environment variables in 'env'.
179180
Priority: Secret credentials > Environment variables > RustFS built-in defaults.
180181
For production use, always configure credentials via Secret or environment variables.

docs/operator-user-guide.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -455,7 +455,7 @@ spec:
455455

456456
### 7.3 Credentials
457457

458-
For production, use `spec.credsSecret`. The Secret must be in the same namespace as the Tenant and contain UTF-8 `accesskey` and `secretkey` keys. Both values must be at least 8 characters.
458+
For production, use `spec.credsSecret`. The Secret must be in the same namespace as the Tenant and contain `accesskey` and `secretkey` keys. Both values must be valid UTF-8 without NUL bytes and at least 8 UTF-8 bytes after trimming.
459459

460460
```yaml
461461
apiVersion: v1
@@ -1154,7 +1154,8 @@ Common blocked reasons:
11541154
| `InvalidPoolSpec` | Pool count, total volume count, pool name, and immutable fields. |
11551155
| `CredentialSecretNotFound` | Secret exists in the Tenant namespace. |
11561156
| `CredentialSecretMissingKey` | Secret contains `accesskey` and `secretkey`. |
1157-
| `CredentialSecretTooShort` | Both credential values are at least 8 characters. |
1157+
| `CredentialSecretInvalidEncoding` | Both credential values are valid UTF-8 without NUL bytes. |
1158+
| `CredentialSecretTooShort` | Both credential values are at least 8 UTF-8 bytes after trimming. |
11581159
| `KmsSecretNotFound` / `KmsSecretMissingKey` | KMS Secret exists and contains required keys, such as Vault `vault-token` or the Local KMS `masterKeySecretRef.key`. |
11591160
| `CertManagerCrdMissing` / `CertManagerIssuerNotFound` | cert-manager is installed and the issuer exists. |
11601161
| `InvalidWorkloadSecurityProfile` | Fix the seccomp or AppArmor profile type and its `localhostProfile` pairing. |

docs/operator-user-guide.zh-CN.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -431,7 +431,7 @@ spec:
431431

432432
### 7.3 凭据配置
433433

434-
生产环境建议使用 `spec.credsSecret`。Secret 必须与 Tenant 在同一 namespace,并包含 UTF-8 编码的 `accesskey` 和 `secretkey` 两个 key,两个值长度都至少为 8 个字符
434+
生产环境建议使用 `spec.credsSecret`。Secret 必须与 Tenant 在同一 namespace,并包含 `accesskey` 和 `secretkey` 两个 key;两个值都必须是不含 NUL 字节的有效 UTF-8,且 trim 后至少为 8 个 UTF-8 字节
435435

436436
```yaml
437437
apiVersion: v1
@@ -1111,7 +1111,8 @@ kubectl logs -n rustfs-system \
11111111
| `InvalidPoolSpec` | Pool 数量、总卷数、pool 名称和不可变字段。 |
11121112
| `CredentialSecretNotFound` | Secret 是否存在于 Tenant namespace。 |
11131113
| `CredentialSecretMissingKey` | Secret 是否包含 `accesskey` 和 `secretkey`。 |
1114-
| `CredentialSecretTooShort` | 两个凭据值是否都至少 8 个字符。 |
1114+
| `CredentialSecretInvalidEncoding` | 两个凭据值是否都是不含 NUL 字节的有效 UTF-8。 |
1115+
| `CredentialSecretTooShort` | 两个凭据值 trim 后是否都至少为 8 个 UTF-8 字节。 |
11151116
| `KmsSecretNotFound` / `KmsSecretMissingKey` | KMS Secret 是否存在,并包含必要 key,例如 Vault 的 `vault-token` 或 Local KMS 的 `masterKeySecretRef.key`。 |
11161117
| `CertManagerCrdMissing` / `CertManagerIssuerNotFound` | cert-manager 是否安装,issuer 是否存在。 |
11171118
| `InvalidWorkloadSecurityProfile` | 修正 seccomp 或 AppArmor profile 类型及其与 `localhostProfile` 的组合。 |

examples/secret-credentials-tenant.yaml

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,11 @@ type: Opaque
2323
stringData:
2424
# RustFS admin access key (username)
2525
# ⚠️ IMPORTANT: Change these default values for production deployments!
26-
# REQUIRED: Must be at least 8 characters long
26+
# REQUIRED: Must be at least 8 UTF-8 bytes after trimming
2727
accesskey: "rustfsadmin123"
2828

2929
# RustFS admin secret key (password)
30-
# REQUIRED: Must be at least 8 characters long
30+
# REQUIRED: Must be at least 8 UTF-8 bytes after trimming
3131
# Recommendation: Use at least 16 characters with mixed case, numbers, and symbols
3232
secretkey: "rustfsadmin123"
3333

@@ -199,7 +199,8 @@ spec:
199199
# Note: The operator will retry every 60 seconds until keys are present
200200

201201
# Issue: Credential validation error - keys too short
202-
# Solution: Ensure both accesskey and secretkey are at least 8 characters
202+
# Solution: Ensure both values are valid UTF-8 without NUL bytes and at least
203+
# 8 UTF-8 bytes after trimming
203204
# kubectl get secret rustfs-credentials -o jsonpath='{.data.accesskey}' | base64 -d | wc -c
204205
# kubectl get secret rustfs-credentials -o jsonpath='{.data.secretkey}' | base64 -d | wc -c
205206
# Note: The operator will retry every 60 seconds until credentials meet requirements
@@ -232,8 +233,8 @@ spec:
232233
# Credential validation errors (require user action):
233234
# - Secret not found
234235
# - Missing required keys (accesskey/secretkey)
235-
# - Invalid UTF-8 encoding
236-
# - Credentials too short (less than 8 characters)
236+
# - Invalid UTF-8 encoding or NUL bytes
237+
# - Credentials too short (less than 8 UTF-8 bytes after trimming)
237238
# Retry interval: 60 seconds (reduces log/event spam while you fix the issue)
238239
#
239240
# Transient errors (may self-resolve):

examples/tenant-4nodes.yaml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,8 @@ spec:
6868
value: "true"
6969

7070
---
71-
# Credentials Secret for RustFS (keys must be at least 8 characters)
71+
# Credentials Secret for RustFS (values must be valid UTF-8 without NUL bytes
72+
# and at least 8 UTF-8 bytes after trimming)
7273
# Example: accesskey admin123, secretkey admin12345 (dev only)
7374
apiVersion: v1
7475
kind: Secret

src/context.rs

Lines changed: 68 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ pub enum Error {
5454
CredentialSecretInvalidEncoding { secret_name: String, key: String },
5555

5656
#[snafu(display(
57-
"credential secret '{}' key '{}' must be at least 8 characters (got {} characters)",
57+
"credential secret '{}' key '{}' must be at least 8 UTF-8 bytes after trimming (got {} bytes)",
5858
secret_name,
5959
key,
6060
length
@@ -298,7 +298,7 @@ fn validate_secret_utf8_non_blank(
298298
const RUSTFS_DEFAULT_CREDENTIAL_VALUE: &str = "rustfsadmin";
299299

300300
const CREDENTIAL_SECRET_KEYS: [&str; 2] = ["accesskey", "secretkey"];
301-
const MIN_CREDENTIAL_LENGTH: usize = 8;
301+
const MIN_CREDENTIAL_LENGTH_BYTES: usize = 8;
302302

303303
fn validate_credential_secret_data(secret: &Secret, secret_name: &str) -> Result<(), Error> {
304304
for key in CREDENTIAL_SECRET_KEYS {
@@ -315,8 +315,15 @@ fn validate_credential_secret_data(secret: &Secret, secret_name: &str) -> Result
315315
secret_name: secret_name.to_string(),
316316
key: key.to_string(),
317317
})?;
318-
let length = value.len();
319-
if length < MIN_CREDENTIAL_LENGTH {
318+
if value.contains('\0') {
319+
return Err(Error::CredentialSecretInvalidEncoding {
320+
secret_name: secret_name.to_string(),
321+
key: key.to_string(),
322+
});
323+
}
324+
325+
let length = value.trim().len();
326+
if length < MIN_CREDENTIAL_LENGTH_BYTES {
320327
return CredentialSecretTooShortSnafu {
321328
secret_name: secret_name.to_string(),
322329
key: key.to_string(),
@@ -643,8 +650,8 @@ impl Context {
643650
/// # Validation Rules
644651
/// - Secret must exist in the same namespace as the Tenant
645652
/// - Secret must contain both `accesskey` and `secretkey` keys
646-
/// - Both keys must be valid UTF-8 strings
647-
/// - Both keys must be at least 8 characters long
653+
/// - Both keys must be valid UTF-8 strings without NUL bytes
654+
/// - Both keys must be at least 8 UTF-8 bytes after trimming
648655
///
649656
/// # Returns
650657
/// - `Ok(())` if Secret is valid or not configured
@@ -890,8 +897,8 @@ mod credential_secret_validation_tests {
890897
fn credential_secret_accepts_valid_values() {
891898
let secret = Secret {
892899
data: Some(BTreeMap::from([
893-
("accesskey".to_string(), ByteString(b"access01".to_vec())),
894-
("secretkey".to_string(), ByteString(b"secret01".to_vec())),
900+
("accesskey".to_string(), ByteString(b" access01 ".to_vec())),
901+
("secretkey".to_string(), ByteString(b" secret01 ".to_vec())),
895902
])),
896903
..Default::default()
897904
};
@@ -944,11 +951,21 @@ mod credential_secret_validation_tests {
944951
}
945952

946953
#[test]
947-
fn credential_secret_values_must_be_valid_utf8() {
954+
fn credential_secret_values_must_be_environment_safe_utf8() {
948955
let valid_value = ByteString(b"valid-key".to_vec());
949956
for (access_key, secret_key, invalid_key) in [
950957
(ByteString(vec![0xff]), valid_value.clone(), "accesskey"),
951958
(valid_value.clone(), ByteString(vec![0xff]), "secretkey"),
959+
(
960+
ByteString(b"valid\0key".to_vec()),
961+
valid_value.clone(),
962+
"accesskey",
963+
),
964+
(
965+
valid_value.clone(),
966+
ByteString(b"valid\0key".to_vec()),
967+
"secretkey",
968+
),
952969
] {
953970
let secret = Secret {
954971
data: Some(BTreeMap::from([
@@ -970,13 +987,32 @@ mod credential_secret_validation_tests {
970987
#[test]
971988
fn credential_secret_values_must_be_at_least_eight_bytes() {
972989
let valid_value = ByteString(b"valid-key".to_vec());
973-
for (access_key, secret_key, invalid_key) in [
974-
(ByteString(Vec::new()), valid_value.clone(), "accesskey"),
975-
(valid_value.clone(), ByteString(Vec::new()), "secretkey"),
990+
for (access_key, secret_key, invalid_key, expected_length) in [
991+
(ByteString(Vec::new()), valid_value.clone(), "accesskey", 0),
992+
(valid_value.clone(), ByteString(Vec::new()), "secretkey", 0),
976993
(
977994
ByteString(b"short".to_vec()),
978995
valid_value.clone(),
979996
"accesskey",
997+
5,
998+
),
999+
(
1000+
ByteString(b" ".to_vec()),
1001+
valid_value.clone(),
1002+
"accesskey",
1003+
0,
1004+
),
1005+
(
1006+
ByteString(b" short ".to_vec()),
1007+
valid_value.clone(),
1008+
"accesskey",
1009+
5,
1010+
),
1011+
(
1012+
valid_value.clone(),
1013+
ByteString(b" short ".to_vec()),
1014+
"secretkey",
1015+
5,
9801016
),
9811017
] {
9821018
let secret = Secret {
@@ -993,11 +1029,29 @@ mod credential_secret_validation_tests {
9931029
Error::CredentialSecretTooShort {
9941030
secret_name,
9951031
key,
996-
..
997-
} if secret_name == "creds" && key == invalid_key
1032+
length,
1033+
} if secret_name == "creds"
1034+
&& key == invalid_key
1035+
&& length == expected_length
9981036
));
9991037
}
10001038
}
1039+
1040+
#[test]
1041+
fn credential_secret_length_is_measured_in_trimmed_utf8_bytes() {
1042+
let secret = Secret {
1043+
data: Some(BTreeMap::from([
1044+
(
1045+
"accesskey".to_string(),
1046+
ByteString(" \u{1f510}\u{1f510} ".as_bytes().to_vec()),
1047+
),
1048+
("secretkey".to_string(), ByteString(b"secret01".to_vec())),
1049+
])),
1050+
..Default::default()
1051+
};
1052+
1053+
assert!(validate_credential_secret_data(&secret, "creds").is_ok());
1054+
}
10011055
}
10021056

10031057
#[cfg(test)]

src/status.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ impl StatusError {
6060
Reason::CredentialSecretInvalidEncoding,
6161
ConditionType::CredentialsReady,
6262
format!(
63-
"Credential Secret '{}' key '{}' must contain valid UTF-8",
63+
"Credential Secret '{}' key '{}' must contain valid UTF-8 without NUL bytes",
6464
secret_name, key
6565
),
6666
),
@@ -70,7 +70,7 @@ impl StatusError {
7070
Reason::CredentialSecretTooShort,
7171
ConditionType::CredentialsReady,
7272
format!(
73-
"Credential Secret '{}' key '{}' must be at least 8 characters",
73+
"Credential Secret '{}' key '{}' must be at least 8 UTF-8 bytes after trimming",
7474
secret_name, key
7575
),
7676
),

src/types/v1alpha1/tenant.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,8 @@ pub struct TenantSpec {
181181
// // #[serde(default, skip_serializing_if = "Option::is_none")]
182182
// // pub side_cars: Option<SideCars>,
183183
/// Optional reference to a Secret containing RustFS credentials.
184-
/// The Secret must contain 'accesskey' and 'secretkey' keys (both required, minimum 8 characters each).
184+
/// The Secret must contain 'accesskey' and 'secretkey' keys. Both values must be valid
185+
/// UTF-8 without NUL bytes and at least 8 UTF-8 bytes after trimming.
185186
/// If not specified, credentials can be provided via environment variables in 'env'.
186187
/// Priority: Secret credentials > Environment variables > RustFS built-in defaults.
187188
/// For production use, always configure credentials via Secret or environment variables.

0 commit comments

Comments
 (0)