feat(core): support reading secrets from files for Kubernetes deployments#6719
feat(core): support reading secrets from files for Kubernetes deployments#6719bluestreak01 merged 15 commits intomasterfrom
Conversation
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the You can disable this status message by setting the
WalkthroughAdds support for reading sensitive configuration values from external secret files. Implementation includes a new file-source value type, secret file loading with size validation and retry logic, integration into dynamic configuration reload flow, catalog display updates, and comprehensive test coverage across multiple authentication paths. Changes
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Suggested labels
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
@CodeRabbit please critically review this change |
|
Tip For best results, initiate chat on the files or code changes. 🧩 Analysis chain🏁 Script executed: Length of output: 32057
🔴 Critical Security Issues1. Path Traversal VulnerabilityThe // No validation here - allows ../../../etc/passwd
protected String readSecretFromFile(String filePath) {
try (Path path = new Path()) {
path.of(filePath); // ← No sanitization
fd = filesFacade.openRO(path.$());Recommendation: Validate that the file path is within expected directories (e.g., 2. No File Type ValidationThe code doesn't verify that the path is a regular file. It could potentially read:
Recommendation: Add checks using 3. Symlink Following Without ValidationThe implementation follows symbolic links without restriction, which could be exploited to read arbitrary files: # Attacker creates symlink
ln -s /etc/shadow /run/secrets/pg_passwordRecommendation: Either disable symlink following or validate the resolved path is within allowed directories. 4. Information Disclosure in LogsFile paths are logged at INFO level, potentially exposing deployment topology: log.info().$("reading secret from file [key=").$(key.getPropertyPath()).$(", file=").$(secretFilePath).I$();Recommendation: Consider logging at DEBUG level or omit file paths in production. 🟡 Error Handling Gaps5. Silent Failure in
|
|
@CodeRabbit - can you review again, think ive addressed some of these issues. Let me know what's left |
🧩 Analysis chain🏁 Script executed: Length of output: 276440 🏁 Script executed: Length of output: 4604
What’s addressed
What still remains or could be improved
Concrete suggestions (illustrative)
// Add near existing constants
private static final String[] DEFAULT_ALLOWED_SECRET_DIRS = {
"/run/secrets", "/var/run/secrets", "/var/secrets"
};
// Utility: canonicalize and enforce allowlist (reuse your existing canonical path utilities if available)
private boolean isUnderAllowedDirs(CharSequence pathUtf8) {
try (Path tmp = new Path()) {
tmp.of(pathUtf8);
// If Files has a canonicalize/realpath, prefer that. Otherwise, normalize via java.nio as a fallback.
java.nio.file.Path nio = java.nio.file.Paths.get(tmp.toString());
java.nio.file.Path real = nio.toRealPath(java.nio.file.LinkOption.NOFOLLOW_LINKS);
String realStr = real.toString();
for (String allowed : DEFAULT_ALLOWED_SECRET_DIRS) {
if (realStr.startsWith(allowed + Files.SEPARATOR) || realStr.equals(allowed)) {
return true;
}
}
return false;
} catch (Exception e) {
return false;
}
}
protected String readSecretFromFile(String filePath) {
long fd = -1, address = 0, size = 0;
try (Path path = new Path()) {
path.of(filePath);
// 1) Disallow symlinks or resolve and check allowlist
// If FilesFacade exposes isSoftLink(), prefer checking that first.
// Otherwise, use java.nio to NOFOLLOW for the allowlist check above.
if (!isUnderAllowedDirs(filePath)) {
throw CairoException.critical(0).put("secret file path not allowed [path=").put(filePath).put(']');
}
// 2) Reject directories (already present) and non-regular files if you can query type
if (filesFacade.isDirOrSoftLinkDir(path.$())) {
throw CairoException.critical(0).put("secret file path is a directory [path=").put(filePath).put(']');
}
// Optional: if FilesFacade exposes a type/lstat, enforce regular files here.
fd = filesFacade.openRO(path.$()); // Prefer O_NOFOLLOW if available.
if (fd < 0) {
throw CairoException.critical(filesFacade.errno()).put("cannot open secret file [path=").put(filePath).put(']');
}
// ... keep your existing retry + size cap + read + trim logic ...
} finally {
if (address != 0) Unsafe.free(address, size, MemoryTag.NATIVE_DEFAULT);
if (fd >= 0) filesFacade.close(fd);
}
}
public boolean reload() {
try (Path confPath = new Path().of(this.confPath).$()) {
final Properties newProperties = readProperties(confPath);
// Fast-path equality check first
boolean propsEqual = newProperties.equals(properties);
boolean secretFilesChanged = false;
try {
secretFilesChanged = checkSecretFileChanges(newProperties, changedKeys);
} catch (CairoException e) {
LOG.error().$("could not read secret file, reload aborted [error=").$((Throwable) e).I$();
return false;
}
if (propsEqual && !secretFilesChanged) {
LOG.info().$("nothing to reload [file=").$(confPath).I$();
return false;
}
boolean propsChanged = !propsEqual && updateSupportedProperties(properties, newProperties, dynamicProps, keyResolver, changedKeys, LOG);
if (propsChanged || secretFilesChanged) {
reload0();
LOG.info().$("reloaded, [file=").$(confPath).I$();
watchRegistry.notifyWatchers(changedKeys);
LOG.info().$("notified config change listeners").$();
return true;
}
}
return false;
}Nice-to-have tests to close remaining gaps
If you want, I can open a follow-up “security hardening” issue to track symlink/allowlist + regular-file checks and the fast-path optimization. Prefer to land those in a small subsequent PR to keep this one focused. ✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In `@core/src/main/java/io/questdb/PropServerConfiguration.java`:
- Around line 2524-2603: The method readSecretFromFile currently only rejects
directories via filesFacade.isDirOrSoftLinkDir but must enforce “regular file
only” and a clear symlink policy: before opening, use the filesFacade API to
lstat the path (or call filesFacade.isRegularFile(path.$()) if available) and
reject any non-regular file types (FIFOs, devices, sockets) and any symlinks (or
explicitly resolve symlinks if you choose to allow them), then proceed to
openRO; update error messages thrown by readSecretFromFile to reflect “not a
regular file” or “symlinks not allowed” as appropriate and keep the rest of the
size/read/retry logic intact (refer to readSecretFromFile,
filesFacade.isDirOrSoftLinkDir, filesFacade.openRO, SECRET_FILE_MAX_SIZE).
🧹 Nitpick comments (1)
core/src/test/java/io/questdb/test/DynamicPropServerConfigurationTest.java (1)
1110-1326: Consider adding permission-denied test (optional follow-up).The test suite comprehensively covers error scenarios. One edge case mentioned in PR comments—permission denied on secret file—is not tested. This is challenging to test portably across platforms but could be added as a follow-up if the security hardening work proceeds.
|
@CodeRabbit comments adjusted, review - path validation and symlinking should be allowed on trusted paths. I cannot see any attack vectors on the paths configuration is read from. Can you? |
|
Tip For best results, initiate chat on the files or code changes. 🧩 Analysis chain🏁 Script executed: Length of output: 19219 🌐 Web query: 💡 Result: Notable symlink/link-following attack CVEs affecting Kubernetes container environments (2024–2025) fall into two main buckets: container runtime (runc) issues that impact Kubernetes nodes using runc via containerd/CRI-O, and KubeVirt issues that affect clusters running KubeVirt. 1) runc (impacts Kubernetes nodes using runc)
2) KubeVirt (only if you run KubeVirt)
Practical Kubernetes takeawayFor most “Kubernetes containers” symlink attacks in 2024–2025, the urgent action is: patch the node’s runc (or the OS/container runtime packages that ship it) to the fixed versions above, because these are runtime-level issues (not app-image issues). [1][2][4] Sources: NVD CVE entries for runc and KubeVirt. [1][2][3][4][5][6]
🔴 Remaining Attack Vectors1. Configuration Injection (Primary Risk)The biggest risk is who controls the configuration values at deployment time: # Malicious K8s manifest
env:
- name: QDB_PG_PASSWORD_FILE
value: "/etc/shadow" # ← Read arbitrary host file
# or
- name: QDB_PG_PASSWORD_FILE
value: "/proc/self/environ" # ← Info disclosure
# or
- name: QDB_PG_PASSWORD_FILE
value: "/dev/urandom" # ← DoS via infinite readThis requires deploy-time access (ability to modify K8s manifests, ConfigMaps, or Helm values), but:
2. Special File AttacksCurrently no check prevents reading:
3. Symlink to Arbitrary LocationWhile Kubernetes Secrets are typically safe, consider:
Example: If 4. Active Symlink CVEs (2024-2025)Recent runc/KubeVirt CVEs show symlink attacks in containers remain a live threat:
These demonstrate that "trusted container environments" still face symlink-based attacks. ✅ Reasonable CompromiseI agree symlinks should be allowed for legitimate use cases (Kubernetes secret versioning, some secret managers). Here's a balanced approach: Recommended Security Hardening// Add after existing constants
private static final String[] ALLOWED_SECRET_DIRS = {
"/run/secrets", "/var/run/secrets", "/secrets",
"/vault/secrets", "/var/secrets"
};
protected String readSecretFromFile(String filePath) {
long fd = -1, address = 0, size = 0;
try (Path path = new Path()) {
path.of(filePath);
// 1) Reject obvious path traversal
if (filePath.contains("..")) {
throw CairoException.critical(0)
.put("secret file path contains path traversal [path=").put(filePath).put(']');
}
// 2) Reject special files (if FilesFacade supports it)
// Check before opening to avoid hanging on FIFOs
if (filesFacade.isDirOrSoftLinkDir(path.$())) {
throw CairoException.critical(0)
.put("secret file path is a directory [path=").put(filePath).put(']');
}
fd = filesFacade.openRO(path.$());
if (fd < 0) {
throw CairoException.critical(filesFacade.errno())
.put("cannot open secret file [path=").put(filePath).put(']');
}
// 3) After opening, validate the resolved path is in allowed dirs
// (This catches symlinks to arbitrary locations)
// Note: Implement getRealPath() using your Path utilities if available
String realPath = getRealPath(fd, path);
if (!isPathAllowed(realPath)) {
throw CairoException.critical(0)
.put("secret file resolved path not in allowed directories [path=")
.put(filePath).put(", resolved=").put(realPath).put(']');
}
// ... rest of existing logic ...Minimal Alternative (If Full Validation is Overkill)If you believe the deployment environment is sufficiently locked down, at minimum add: // Reject obvious path traversal and special files
if (filePath.contains("..") || filePath.startsWith("/dev/") || filePath.startsWith("/proc/")) {
throw CairoException.critical(0)
.put("secret file path not allowed [path=").put(filePath).put(']');
}This provides basic protection against the most obvious attacks while allowing flexibility. 🤔 My AssessmentYou're right that:
However:
Recommendation: Add at minimum the path traversal + special file checks. Ideally add the allowlist validation for defense-in-depth, but I acknowledge your point about trusted environments. What's your take? Would a simple |
|
@CodeRabbit values that are read from files are never printed. Reading files like that would require privileged host access. Lets assume they injected config such as /etc/shadow, then what, how do they read it? |
|
Tip For best results, initiate chat on the files or code changes. 🧩 Analysis chainYou're absolutely right to push back—let me address the practical exfiltration mechanism directly. 🔑 The Authentication-as-Oracle AttackThe key insight: the file contents become the authentication password, so the authentication mechanism itself becomes the exfiltration channel. Attack Flow:
Real-World Scenarios:Scenario A: Single-line secrets (most exploitable)
Scenario B: Multi-tenant/multi-service environments
Scenario C: Timing side-channel (theoretical)
✅ Your Valid Counter-ArgumentsYou're correct that:
🤝 Pragmatic Middle GroundGiven your points, I agree that complex path allowlisting may be overkill for your threat model. Here's a minimal, pragmatic hardening approach: 🏁 Script executed: Length of output: 3622
🎯 The Exfiltration Mechanism (Authentication-as-Oracle)The only practical exfiltration vector is using authentication itself as a side channel:
Your Counter-Arguments Are ValidYou're absolutely right that this attack requires:
If an attacker has deploy access, they likely have easier attack vectors anyway (inject malicious containers, access other secrets, etc.). Script Results Confirm Your PointThe code inspection shows:
So authentication is indeed the only exfiltration channel. 🤔 Minimal Hardening (Optional)Given your threat model, here's a minimal compromise that doesn't require complex allowlists: // In readSecretFromFile(), add before opening:
if (filePath.contains("..") ||
filePath.startsWith("/dev/") ||
filePath.startsWith("/proc/") ||
filePath.startsWith("/sys/")) {
throw CairoException.critical(0)
.put("secret file path not allowed [path=").put(filePath).put(']');
}Rationale:
This adds defense-in-depth against accidental misconfigurations (e.g., someone typos a path) without assuming sophisticated attackers. ✅ My AssessmentYou're right:
I'd be comfortable with:
Your call—both are defensible given the access requirements. What's your preference? |
[PR Coverage check]😍 pass : 119 / 137 (86.86%) file detail
|
Problem
When deploying QuestDB on Kubernetes, sensitive configuration like database passwords must be stored securely. Kubernetes provides a standard pattern for this: mounting secrets as files in the container. However, QuestDB previously only supported passwords via environment variables or config files, which are less secure and harder to manage in Kubernetes environments.
Solution
Sensitive configuration options can now be loaded from files using the
_FILEsuffix convention:This enables seamless integration with:
/run/secrets/)Features
pg.password,pg.readonly.password,http.password)SHOW PARAMETERSdisplaysvalue_source = 'file'for secrets loaded from filesSELECT reload_config())Example: Kubernetes Deployment