Skip to content

feat(core): support reading secrets from files for Kubernetes deployments#6719

Merged
bluestreak01 merged 15 commits intomasterfrom
vi_k8s
Feb 2, 2026
Merged

feat(core): support reading secrets from files for Kubernetes deployments#6719
bluestreak01 merged 15 commits intomasterfrom
vi_k8s

Conversation

@bluestreak01
Copy link
Copy Markdown
Member

@bluestreak01 bluestreak01 commented Jan 28, 2026

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 _FILE suffix convention:

# Environment variable pointing to secret file
export QDB_PG_PASSWORD_FILE=/run/secrets/pg_password

# Or in server.conf
pg.password.file=/run/secrets/pg_password

This enables seamless integration with:

  • Kubernetes Secrets mounted as volumes
  • Docker Secrets (/run/secrets/)
  • HashiCorp Vault agent injector
  • Any secrets management system that provides file-based secrets

Features

  • Works with all sensitive properties (pg.password, pg.readonly.password, http.password)
  • File contents are automatically trimmed (whitespace removed)
  • SHOW PARAMETERS displays value_source = 'file' for secrets loaded from files
  • Secrets are reloaded when file contents change (via SELECT reload_config())
  • Supports switching from direct password to file-based without restart

Example: Kubernetes Deployment

apiVersion: v1
kind: Secret
metadata:
  name: questdb-secrets
type: Opaque
data:
  pg-password: cXVlc3RkYg==  # base64 encoded

---
apiVersion: apps/v1
kind: Deployment
spec:
  template:
    spec:
      containers:
        - name: questdb
          env:
            - name: QDB_PG_PASSWORD_FILE
              value: /secrets/pg-password
          volumeMounts:
            - name: secrets
              mountPath: /secrets
              readOnly: true
      volumes:
        - name: secrets
          secret:
            secretName: questdb-secrets

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Jan 28, 2026

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

  • 🔍 Trigger a full review

Walkthrough

Adds 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

Cohort / File(s) Summary
Configuration Constants & Value Source Tracking
core/src/main/java/io/questdb/ConfigPropertyValue.java
Added VALUE_SOURCE_FILE = 3 constant to track configuration values sourced from external files, expanding the value source enum-like set.
Secret File Reading Implementation
core/src/main/java/io/questdb/PropServerConfiguration.java
Implemented secret file handling: added constants (SECRET_FILE_PROPERTY_SUFFIX, SECRET_FILE_ENV_VAR_SUFFIX, SECRET_FILE_MAX_SIZE), getSecretFilePath() to resolve file paths from env/properties, and readSecretFromFile() with size limits, retry logic, UTF-8 validation, and resource cleanup. Enhanced getString() to attempt secret file loading for sensitive properties.
Dynamic Configuration Reload
core/src/main/java/io/questdb/DynamicPropServerConfiguration.java
Added checkSecretFileChanges() helper to detect file-based secret modifications. Integrated secret-file change detection into reload flow alongside property changes, with appropriate error handling and logging for sensitive property reloads.
Catalog Display Updates
core/src/main/java/io/questdb/griffin/engine/functions/catalogue/ShowParametersCursorFactory.java
Refactored switch statements to switch expressions in ShowParametersRecordCursor. Added explicit handling for FILE value source in column 3 alongside DEFAULT, CONF, and ENV mappings.
Dynamic Configuration Test Suite
core/src/test/java/io/questdb/test/DynamicPropServerConfigurationTest.java
Added 14 new tests covering secret file loading, reloading, trimming, UTF-8 validation, size limits, file deletion, empty files, directory errors, and password switching between file-based and conf-based sources for PGWire and HTTP authentication.
Integration Tests
core/src/test/java/io/questdb/test/ServerMainTest.java
Added two tests validating secret file loading via environment variables and properties, verifying password values and correct value_source reporting in show parameters output.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Suggested labels

Core

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 10.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and concisely describes the main feature: adding support for reading secrets from files to enable Kubernetes deployments, which aligns with the comprehensive changes across configuration, reloading, and test files.
Description check ✅ Passed The description is well-detailed and directly related to the changeset, explaining the problem, solution, supported properties, features, and providing concrete Kubernetes examples that match the implementation in PropServerConfiguration, DynamicPropServerConfiguration, and test files.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch vi_k8s

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@bluestreak01 bluestreak01 changed the title chore(core): support reading secrets from files for Kubernetes deployments feat(core): support reading secrets from files for Kubernetes deployments Jan 28, 2026
@bluestreak01
Copy link
Copy Markdown
Member Author

@CodeRabbit please critically review this change

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Jan 28, 2026

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash

# Get the PR diff to understand what changed
gh pr diff 6719 --repo questdb/questdb

echo "=== Listing all changed files ==="
gh pr view 6719 --repo questdb/questdb --json files --jq '.files[].path'

echo -e "\n=== Finding test files related to PropServerConfiguration ==="
fd -t f -e java -e kt | xargs rg -l "PropServerConfiguration" | grep -i test

echo -e "\n=== Finding test files that test file-based secrets ==="
rg -l "PASSWORD_FILE|password.*file|_FILE" --type java --type kotlin | grep -i test

Length of output: 32057


@bluestreak01, I'll provide a critical review of this change. The feature implementation has several security vulnerabilities and insufficient test coverage for a security-sensitive feature.

🔴 Critical Security Issues

1. Path Traversal Vulnerability

The readSecretFromFile() method accepts file paths without validation. An attacker could potentially use path traversal:

// 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., /run/secrets/, /var/secrets/). Consider using a whitelist approach or canonicalize paths and check boundaries.

2. No File Type Validation

The code doesn't verify that the path is a regular file. It could potentially read:

  • Directories (will fail but waste resources)
  • Character/block devices (could hang or expose system info)
  • FIFOs/pipes (could hang indefinitely)
  • Symlinks to unauthorized locations

Recommendation: Add checks using filesFacade to verify it's a regular file before opening.

3. Symlink Following Without Validation

The implementation follows symbolic links without restriction, which could be exploited to read arbitrary files:

# Attacker creates symlink
ln -s /etc/shadow /run/secrets/pg_password

Recommendation: Either disable symlink following or validate the resolved path is within allowed directories.

4. Information Disclosure in Logs

File 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 Gaps

5. Silent Failure in checkSecretFileChanges()

When file reading fails during reload, the error is logged but reload continues:

} catch (CairoException e) {
    LOG.error().$("could not read secret file for reload check...").I$();
    continue;  // ← Silent failure
}

This means a deleted or corrupted secret file won't prevent startup with stale credentials.

Recommendation: Consider making this a hard failure or at least setting a flag that the configuration is degraded.

6. Missing UTF-8 Validation

The code uses Utf8s.stringFromUtf8Bytes() without catching potential exceptions from malformed UTF-8:

return Utf8s.stringFromUtf8Bytes(address, address + size).trim();

Recommendation: Wrap in try-catch and provide clear error message.

7. Empty File Behavior

Files containing only whitespace will result in empty password after trimming:

if (size == 0) {
    return "";  // ← Intentional empty password?
}
// ...
return Utf8s.stringFromUtf8Bytes(address, address + size).trim();  // ← Could also be empty

Recommendation: Decide if empty passwords should be rejected and validate accordingly.

🟠 Test Coverage Deficiencies

The 19.15% coverage for security-critical code is concerning. Missing test cases:

Error Scenarios (0% covered):

  • ❌ File not found
  • ❌ Permission denied (chmod 000)
  • ❌ File exceeds 64KB limit
  • ❌ File is a directory
  • ❌ File is a device/pipe/socket
  • ❌ File contains invalid UTF-8
  • ❌ File is deleted during reload
  • ❌ File permissions change to unreadable
  • ❌ Disk I/O errors
  • ❌ Out of memory during file read

Edge Cases (0% covered):

  • ❌ Empty file
  • ❌ File with only whitespace
  • ❌ File with trailing newlines (common with Kubernetes secrets)
  • ❌ Symbolic link to another secret file
  • ❌ Symbolic link to non-existent file
  • ❌ Relative vs absolute paths
  • ❌ Concurrent file reads during reload
  • ❌ File modified between size check and read

Integration Tests Missing:

  • http.password from file
  • pg.readonly.password from file
  • ❌ Multiple passwords from files simultaneously
  • ❌ Mixed file and direct password configs
  • ❌ Reload when switching from file to direct password (reverse direction)

🔵 Design & Performance Concerns

8. Removed Optimization

-        if (newProperties.equals(oldProperties)) {
-            return false;
-        }

This early-return optimization was removed, meaning every reload now iterates through all properties even when nothing changed. This could impact systems with frequent reload calls.

Recommendation: Restore optimization but add separate check for secret file changes.

9. N+1 File Reads on Reload

checkSecretFileChanges() reads each secret file individually during reload. With multiple secrets, this creates multiple I/O operations.

Recommendation: Consider batching or at least document the I/O overhead.

10. Race Condition Window

Between checking file size and reading contents, the file could be modified:

size = filesFacade.length(fd);  // ← Time-of-check
// ...
long bytesRead = filesFacade.read(fd, address, size, 0);  // ← Time-of-use
if (bytesRead != size) {  // This catches it, but...

The check handles this, but malformed data could still be read if file shrinks.

📋 Recommendations Summary

High Priority:

  1. Add path validation to prevent traversal attacks
  2. Validate file is a regular file before reading
  3. Add comprehensive error handling tests (target 90%+ coverage)
  4. Document security considerations for file paths
  5. Consider fail-fast on deleted secret files during reload

Medium Priority:
6. Add edge case tests (empty, whitespace, invalid UTF-8)
7. Restore the newProperties.equals() optimization
8. Add integration tests for all password types
9. Reduce logging verbosity for file paths
10. Add validation for empty passwords

Low Priority:
11. Document I/O overhead of multiple secret files
12. Consider restricting symlink following
13. Add metrics for secret reload failures

Would you like me to create a follow-up issue to track these security improvements, or would you prefer to address them in this PR?

@bluestreak01
Copy link
Copy Markdown
Member Author

@CodeRabbit - can you review again, think ive addressed some of these issues. Let me know what's left

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Feb 1, 2026

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash

# Get the latest PR diff
echo "=== Latest PR Diff ==="
gh pr diff 6719 --repo questdb/questdb

echo -e "\n=== Changed files ==="
gh pr view 6719 --repo questdb/questdb --json files --jq '.files[].path'

echo -e "\n=== Checking PropServerConfiguration for security validations ==="
# Look for path validation, file type checks, etc.
rg -A 10 -B 2 "readSecretFromFile" --type java

echo -e "\n=== Checking for symlink handling ==="
rg "symlink|canonical|normalize|isRegularFile" --type java

echo -e "\n=== Checking error handling in checkSecretFileChanges ==="
rg -A 20 "checkSecretFileChanges" --type java

echo -e "\n=== Checking for empty password validation ==="
rg "trim\(\)|isEmpty|isBlank" --type java | grep -i secret -A 2 -B 2

echo -e "\n=== Looking for test files ==="
fd -e java -e kt | xargs rg -l "readSecretFromFile|PASSWORD_FILE" | grep -i test

echo -e "\n=== Checking test coverage scenarios ==="
rg -A 5 "testSecret|test.*[Ff]ile|test.*[Pp]assword" --type java | grep -i test

Length of output: 276440


🏁 Script executed:

#!/bin/bash

# Check the latest test coverage more specifically
echo "=== Checking test methods related to secret files ==="
rg "public void test.*[Ss]ecret|public void test.*[Pp]assword.*[Ff]ile" --type java -A 1 core/src/test/java/io/questdb/test/DynamicPropServerConfigurationTest.java

echo -e "\n=== Checking error handling for deleted files ==="
rg "testSecretFileDeleted|testSecretFileNotFound" --type java -A 10 core/src/test/java/io/questdb/test/

echo -e "\n=== Checking for path validation in readSecretFromFile ==="
rg "canonicalize|realpath|normalize|resolve" --type java core/src/main/java/io/questdb/PropServerConfiguration.java

echo -e "\n=== Check if removed optimization was restored ==="
rg "if.*newProperties\.equals\(oldProperties\)" --type java core/src/main/java/io/questdb/DynamicPropServerConfiguration.java

echo -e "\n=== Check logging level for secret file paths ==="
rg "log\.(info|debug|advisory).*reading secret from file" --type java core/src/main/java/io/questdb/PropServerConfiguration.java

Length of output: 4604


@bluestreak01 Great progress — most high-risk items are now covered. Here’s the status and what’s still worth tightening:

What’s addressed

  • Fail-fast reload on secret file errors: reload aborts on CairoException from secret read; old values remain intact. Tests: deleted/missing file.
  • Size and content handling: 64KB cap; trimming; empty/whitespace supported with advisory log; trailing newlines handled; invalid UTF‑8 fails startup.
  • Directory rejection: directories and soft-link directories are rejected.
  • SHOW PARAMETERS: value_source shows file for secrets; covered via env and property.
  • Live updates: switching between direct and file-based secrets, and multi-secret reloads, are tested.
  • Log minimization: file path no longer logged; only the key.

What still remains or could be improved

  1. Path and symlink hardening
  • Today any path (including symlink to an arbitrary file) is accepted if it’s not a directory. Consider:
    • Canonicalize path then enforce it’s under an allowed prefix (e.g., /run/secrets or a configurable list).
    • Optionally reject symlinks, or resolve and then enforce the allowlist.
    • If your FilesFacade supports O_NOFOLLOW or lstat-style checks, prefer those to avoid TOCTOU on links.
  1. Regular file check to avoid FIFOs/devices
  • There’s no explicit “regular file” check, so a FIFO/char device could block on open/read.
  • If available, use a stat/type check (reject S_IFIFO, S_IFCHR, S_IFBLK, S_IFSOCK). Otherwise, add a best-effort guard (e.g., reject zero-length on first read with a clear error when open would block or when length returns an unsupported value).
  1. Early-return optimization on property equality
  • The original fast-path newProperties.equals(oldProperties) remains removed. You can combine both worlds:
    • If properties equal and no secretFilesChanged, skip reload work; otherwise proceed.
  1. Log level nit
  • “reading secret from file [key=…]” at INFO may be chatty in some environments. Optional: demote to DEBUG.

Concrete suggestions (illustrative)

  • PropServerConfiguration.readSecretFromFile hardening
// 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);
    }
}
  • DynamicPropServerConfiguration.reload fast-path sketch
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

  • Permission denied: simulate openRO returning -1 with errno EACCES using TestFilesFacadeImpl; assert startup fails and reload aborts with old value retained.
  • Symlink policy: add tests for symlink to allowed file (if you choose to allow) and symlink to disallowed target (e.g., outside allowed roots) to ensure it’s rejected.

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 performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@bluestreak01
Copy link
Copy Markdown
Member Author

@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?

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Feb 2, 2026

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Get the latest implementation of readSecretFromFile
echo "=== PropServerConfiguration.readSecretFromFile implementation ==="
rg -A 50 "protected String readSecretFromFile" --type java core/src/main/java/io/questdb/PropServerConfiguration.java

echo -e "\n=== Check for path validation logic ==="
rg -B 5 -A 15 "isDirOrSoftLinkDir|canonical|realpath|normalize" --type java core/src/main/java/io/questdb/PropServerConfiguration.java

echo -e "\n=== Check DynamicPropServerConfiguration for secret file handling ==="
rg -A 30 "checkSecretFileChanges" --type java core/src/main/java/io/questdb/DynamicPropServerConfiguration.java

echo -e "\n=== Look for any symlink-related tests ==="
rg "symlink|Symlink" --type java core/src/test/

echo -e "\n=== Check what file operations FilesFacade supports ==="
rg "interface FilesFacade|class.*FilesFacade" --type java -A 20 | head -100

Length of output: 19219


🌐 Web query:

symlink attacks in Kubernetes containers CVE 2024 2025

💡 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)

  • CVE-2024-45310 (runc) — A race (os.MkdirAll) with shared volumes between containers can be abused to create empty files/directories in arbitrary host paths (symlink/race-style link following). Fixed in runc 1.1.14 and 1.2.0-rc3. NVD explicitly notes it’s exploitable “through Docker and Kubernetes”. [1]
  • CVE-2025-31133 (runc) — Insufficient verification of the inode used for masking (/dev/null) enables an arbitrary mount “gadget” / maskedPaths bypass, leading to host info disclosure, DoS, or container escape. Fixed in runc 1.2.8 / 1.3.3 / 1.4.0-rc.3. [2]
  • CVE-2025-52565 (runc) — Insufficient checks when bind-mounting /dev/pts/$n/dev/console can be abused to bind-mount paths that should be masked/read-only onto attacker-writable paths; can lead to DoS or container breakout (post-pivot_root). (Same fix line: 1.2.8 / 1.3.3 / 1.4.0-rc.3). [3]
  • CVE-2025-52881 (runc) — A race with shared mounts can misdirect runc writes intended for /proc to other procfs files, potentially via symlinks in tmpfs; fixed in runc 1.2.8 / 1.3.3 / 1.4.0-rc.3. [4]

2) KubeVirt (only if you run KubeVirt)

  • CVE-2025-64433 (KubeVirt) — Improper symlink handling when mounting PVC disks into a VM can allow a VM to read arbitrary files from the virt-launcher pod filesystem (if attacker controls PVC contents). [5]
  • CVE-2025-64437 (KubeVirt)virt-handler doesn’t verify whether launcher-sock is a symlink; can be exploited to change ownership of arbitrary host files (to UID 107) if attacker controls the virt-launcher pod filesystem. Fixed in KubeVirt 1.5.3 / 1.6.1. [6]

Practical Kubernetes takeaway

For 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]


@bluestreak01 You raise a fair point about trusted paths, but there are still attack vectors worth considering for defense-in-depth:

🔴 Remaining Attack Vectors

1. 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 read

This requires deploy-time access (ability to modify K8s manifests, ConfigMaps, or Helm values), but:

  • In multi-tenant clusters, namespace admins might have this access without cluster-admin rights
  • Supply-chain attacks could inject malicious manifests
  • Insider threats with limited RBAC could exploit this

2. Special File Attacks

Currently no check prevents reading:

  • FIFOs/pipes → can hang indefinitely waiting for writer
  • Character devices (/dev/random, /dev/zero) → resource exhaustion
  • Block devices → unintended disk access
  • Sockets → unexpected behavior

3. Symlink to Arbitrary Location

While Kubernetes Secrets are typically safe, consider:

  • hostPath mounts (sometimes used with external secret managers)
  • Shared volumes across containers (CVE-2024-45310 exploited this)
  • Volume subPath mounts (can preserve existing symlinks)
  • External secret managers (Vault, AWS Secrets Manager) might use symlinks

Example: If /run/secrets/pg_password is a symlink → /host-root/etc/shadow, QuestDB will happily read it.

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 Compromise

I 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 Assessment

You're right that:

  • Configuration paths are typically controlled by cluster admins with RBAC
  • Kubernetes Secret mounts are generally safe and read-only
  • Symlinks have legitimate uses

However:

  • QuestDB is database infrastructure (high-value target)
  • CVE history shows container symlink attacks are real and ongoing
  • Defense-in-depth: even "trusted" config benefits from validation
  • The validation cost is minimal (one-time at startup/reload)

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 /dev/, /proc/, .. blocklist address your concerns while maintaining flexibility?

@bluestreak01
Copy link
Copy Markdown
Member Author

@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?

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Feb 2, 2026

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

You're absolutely right to push back—let me address the practical exfiltration mechanism directly.

🔑 The Authentication-as-Oracle Attack

The key insight: the file contents become the authentication password, so the authentication mechanism itself becomes the exfiltration channel.

Attack Flow:

  1. Attacker injects malicious config (requires deploy access):

    env:
      - name: QDB_PG_PASSWORD_FILE
        value: "/proc/sys/kernel/random/boot_id"  # 36-char UUID
  2. QuestDB reads the file and uses its contents as pg.password

  3. Attacker connects from network and authenticates:

    # Try authentication with various guesses
    psql -h questdb-service -U admin -W
    # When auth succeeds, attacker has learned the file contents
  4. Authentication success = file content disclosed

Real-World Scenarios:

Scenario A: Single-line secrets (most exploitable)

  • /proc/sys/kernel/random/boot_id (UUID)
  • API tokens stored in files
  • License keys
  • Service account passwords from other systems

Scenario B: Multi-tenant/multi-service environments

  • Attacker has namespace-admin rights in K8s (can deploy apps)
  • QuestDB runs in a different namespace (lateral attack)
  • Attacker deploys malicious config → reads QuestDB pod's filesystem → authenticates to exfiltrate

Scenario C: Timing side-channel (theoretical)

  • Even without successful auth, timing differences during password comparison could leak partial file contents
  • Timing attacks measure "how much time various computations (such as, say, comparing an attacker's given password with the victim's unknown one) take to perform"
  • An attacker can "guess a password by measuring the execution time of a password verification function" by sending "a series of password guesses" and measuring response times

✅ Your Valid Counter-Arguments

You're correct that:

  1. Multi-line files (like /etc/shadow) are impractical—trimming makes them unusable as passwords
  2. Privileged file access + deploy access likely means the attacker already has significant control
  3. The attack requires BOTH deploy-time config injection AND network connectivity to QuestDB
  4. Defense-in-depth has diminishing returns when the deployment environment is already compromised

🤝 Pragmatic Middle Ground

Given your points, I agree that complex path allowlisting may be overkill for your threat model. Here's a minimal, pragmatic hardening approach:


🏁 Script executed:

#!/bin/bash

# Check current readSecretFromFile implementation
echo "=== Current readSecretFromFile implementation ==="
rg -A 40 "protected String readSecretFromFile" --type java core/src/main/java/io/questdb/PropServerConfiguration.java

echo -e "\n=== Check if there are any other potential information leakage vectors ==="
# Check if secret values could leak through error messages
rg "password.*Exception|password.*error|password.*Error" --type java core/src/main/java/io/questdb/ -C 3

echo -e "\n=== Check SHOW PARAMETERS implementation for masking ==="
rg "getStrA.*sensitive|mask|\*\*\*\*" --type java core/src/main/java/io/questdb/griffin/engine/functions/catalogue/ShowParametersCursorFactory.java -C 5

echo -e "\n=== Check if passwords appear in any SQL result sets or logs ==="
rg "pg\.password|http\.password" --type java core/src/main/java/io/questdb/ | grep -v "test" | grep -v "\.conf" | head -20

Length of output: 3622


@bluestreak01 Excellent point—you've identified the key limitation. Let me address it directly:

🎯 The Exfiltration Mechanism (Authentication-as-Oracle)

The only practical exfiltration vector is using authentication itself as a side channel:

  1. Attacker injects QDB_PG_PASSWORD_FILE=/proc/sys/kernel/random/boot_id (deploy access required)
  2. QuestDB reads the file → password becomes the 36-char UUID
  3. Attacker connects via network and tries authentication attempts
  4. Successful authentication = file contents disclosed

Your Counter-Arguments Are Valid

You're absolutely right that this attack requires:

  • ✅ Deploy-time access (to inject malicious config)
  • ✅ Network access to QuestDB (to attempt authentication)
  • ✅ Target file must be single-line and trimmed-friendly (rules out /etc/shadow)

If an attacker has deploy access, they likely have easier attack vectors anyway (inject malicious containers, access other secrets, etc.).

Script Results Confirm Your Point

The code inspection shows:

  • ✅ SHOW PARAMETERS masks sensitive values: if (entry.key.isSensitive()) { yield "****"; }
  • ✅ No password leakage in error messages
  • ✅ Secrets never logged or printed

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:

  • /dev/ → prevents reading devices, FIFOs (can cause hangs)
  • /proc/, /sys/ → blocks common info-disclosure paths
  • .. → rejects obvious path traversal
  • No allowlist needed → maintains flexibility for legitimate secret managers

This adds defense-in-depth against accidental misconfigurations (e.g., someone typos a path) without assuming sophisticated attackers.

✅ My Assessment

You're right:

  • The current implementation with directory checking + 64KB cap + trimming is reasonably secure
  • The authentication-oracle attack requires multiple preconditions that indicate the attacker already has significant access
  • Complex path validation may be overkill for QuestDB's deployment model

I'd be comfortable with:

  • Shipping as-is (current protections are solid)
  • OR adding the minimal 4-line check above (defense against misconfig, not sophisticated attacks)

Your call—both are defensible given the access requirements. What's your preference?

@glasstiger
Copy link
Copy Markdown
Contributor

[PR Coverage check]

😍 pass : 119 / 137 (86.86%)

file detail

path covered line new line coverage
🔵 io/questdb/griffin/engine/functions/catalogue/ShowParametersCursorFactory.java 12 16 75.00%
🔵 io/questdb/PropServerConfiguration.java 60 71 84.51%
🔵 io/questdb/DynamicPropServerConfiguration.java 47 50 94.00%

@bluestreak01 bluestreak01 merged commit 0faa9b4 into master Feb 2, 2026
42 of 44 checks passed
@bluestreak01 bluestreak01 deleted the vi_k8s branch February 2, 2026 19:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants