Skip to content

secrets.json left group-readable under Kubernetes fsGroup (0600 not re-asserted on load) #2075

Description

@Aaronontheweb

Summary

On Linux, netclaw writes secrets.json with owner-only permissions (0600) at write time via AtomicFile (File.SetUnixFileMode(path, UserRead | UserWrite)). The mode is applied only when the file is written — it is never re-asserted when the daemon loads the file. If an external process relaxes the mode after the write, secrets.json can be left group-readable/writable, and netclaw doctor's SecretsJsonDoctorCheck then (correctly) flags it.

The common trigger is a Kubernetes Deployment with securityContext.fsGroup set — the standard pattern for letting a non-root container user write to a mounted PVC. On volume mount the kubelet recursively applies the fsGroup ownership and ORs the owner's permission bits into the group (default FSGroupChangePolicy: Always), turning a 0600 file into 0660 (rw-rw----). Because netclaw does not re-chmod secrets.json on load, the relaxation persists across restarts.

Mechanism / evidence

Observed in a containerized Linux deployment (config dir on a mounted PVC, fsGroup set) running 0.27.0-beta.1:

-rw-------  netclaw netclaw  devices.json   # 0600 — rewritten by DeviceRegistry AFTER mount → re-chmod'd
-rw-rw----  netclaw netclaw  secrets.json   # 0660 — not rewritten this boot; fsGroup relaxation stuck
drwxrwsr-x  ...             (config dir)     # setgid bit applied by fsGroup

netclaw doctor:

[WARN] Secrets JSON: secrets.json has overly permissive file mode (GroupWrite, GroupRead, ...).
       fix: chmod 600 <path>/secrets.json

devices.json is clean because DeviceRegistry rewrites it during startup — i.e. after the mount-time fsGroup pass — so it re-applies 0600. secrets.json is not rewritten on a normal boot, so fsGroup's 0600 → 0660 is the last write and sticks. In short: a write-time-only chmod losing a race with a mount-time relaxation.

Relevant code:

  • src/Netclaw.Configuration/AtomicFile.cs — sets 0600 at write time ("Restrict a file to owner-only read/write (chmod 600) on Linux/macOS; a no-op on Windows").
  • src/Netclaw.Daemon/Security/DeviceRegistry.cs — same call for devices.json (which is why it self-heals).
  • src/Netclaw.Cli/Doctor/SecretsJsonDoctorCheck.cs — the check that flags any Group/Other bit.

Impact

Low-to-moderate, deployment-dependent. In a single-tenant pod the fsGroup group typically has only the app itself as a member, so practical exposure is small — but it is a real at-rest posture regression versus bare metal (where doctor passes), and it makes doctor non-green under a very common Kubernetes deployment pattern.

Proposed fix

Re-assert 0600 on secrets.json when the daemon loads/opens it at startup (mirroring how devices.json effectively self-heals by being rewritten). This is idempotent, matches the existing AtomicFile pattern, defeats the fsGroup relaxation without operators having to drop fsGroup (which is load-bearing for PVC writes), and requires no deployment changes. A one-shot chmod at daemon start — or on first secrets read — is sufficient.

Alternatives considered:

  • Operator-side chmod 600 — does not stick; fsGroup re-applies on every remount.
  • Dropping fsGroup — breaks the non-root user's ability to write the PVC.
  • FSGroupChangePolicy: OnRootMismatch — still relaxes the mode, just less frequently.

Repro

  1. Run netclaw in a Linux container with the config dir on a mounted volume and securityContext.fsGroup set.
  2. Ensure secrets.json already exists on the volume from a prior write (0600).
  3. Restart the pod; the kubelet relaxes it to 0660 on mount.
  4. netclaw doctor[WARN] Secrets JSON ... overly permissive file mode.

Related

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestsecuritySecurity-related changes

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions