Skip to content

feat(agent): make output file permissions configurable - #342

Open
alekc wants to merge 3 commits into
Infisical:mainfrom
alekc:feat/issue-339-agent-file-permissions
Open

feat(agent): make output file permissions configurable#342
alekc wants to merge 3 commits into
Infisical:mainfrom
alekc:feat/issue-339-agent-file-permissions

Conversation

@alekc

@alekc alekc commented Jul 30, 2026

Copy link
Copy Markdown

Description 📣

Fixes #339.

The agent writes rendered secret templates with whatever os.Create gives it, so on a normal umask they land 0644, and Template.Config carries only polling-interval and execute, so there is nothing to set. That makes the agent awkward to adopt anywhere it replaces something that wrote 0600.

The execute hook is not a workaround. A chmod 600 there is valid and runs under sh -c, but the hook is guarded:

if !firstRun && execCommand != "" {

So it is skipped on the first render and only fires once an etag change causes a re-render. The file is world-readable from the moment it is created until the first rotation.

This adds a permission field to the template config and to the file sink config, spelled to match the one certificates[].file-output.* already accepts:

sinks:
  - type: file
    config:
      path: /out/access-token
      permission: "0600"
templates:
  - source-path: /etc/infisical/dotenv.tmpl
    destination-path: /out/app/app.env
    config:
      polling-interval: 60s
      permission: "0600"

The sink is included because WriteTokenToFiles had the same hardcoded 0644 on content more valuable than a rendered template.

Implementation notes:

  • Defaults are unchanged. An unset permission keeps the umask-derived mode for templates and 0644 for sinks, so no existing deployment changes behaviour on upgrade. I did not default templates to 0600 even though the content argues for it, because the agent commonly runs as root while the consuming application reads the file as a different user, and tightening the default breaks those setups with no error from the agent. Glad to change it if you would rather have 0600.

  • Values are validated in parseAgentConfigWithMode, so a malformed mode stops the agent at startup rather than falling back to a looser one at first render, which would be invisible until somebody stats the file:

    templates[0].config.permission: invalid file permission "600x": expected an octal mode such as "0600"
    
  • The octal parser is promoted out of WriteCertificateFiles into a package-level parseFileMode, so there is one implementation rather than two. Two deliberate changes to certificate behaviour come with that: modes above 0777 are now rejected, where the previous strconv.ParseInt accepted them and would set setuid on a private key, and a malformed value now logs a warning before falling back to 0600 instead of being silently swallowed. Unset and valid values behave exactly as before.

No new dependencies. The agent config reference lives on infisical.com/docs rather than in this repo, so the new option is undocumented there; happy to open a docs PR if you point me at the right place.

Type ✨

  • Bug fix
  • New feature
  • Improvement
  • Breaking change
  • Documentation

Tests 🛠️

Added packages/cmd/agent_file_permission_test.go, 20 cases, tagged //go:build unix because file modes are a POSIX concept and os.Chmod on Windows only toggles the read-only bit. It covers octal parsing including every rejection case, WriteBytesToFile and WriteTemplateToFile against both a new and an already-existing file, and config parsing for valid, malformed and omitted values.

The already-existing-file cases are the ones that matter. They are umask-independent, so they isolate the chmod as the mechanism instead of passing by accident on whatever umask the runner has. The omitted-permission cases compare against a reference file created with plain os.Create in the same temp dir, which pins "default unchanged" to something stronger than a hardcoded 0644.

To reproduce:

go build ./...
go test -vet=off -count=1 ./packages/cmd/... -run 'TestParseFileMode|TestWriteBytesToFile|TestWriteTemplateToFile|TestParseAgentConfig' -v
gofmt -l packages/cmd/agent.go packages/cmd/agent_file_permission_test.go

-vet=off is needed because go vet ./packages/cmd/ already fails on main with four non-constant format string findings in run.go, unrelated to this change. I left them alone, and confirmed this change adds none by diffing vet output with and without the patch. Happy to fix those separately if useful.

Two things I want to be straight about. The create-with-mode behaviour above is not unit tested and I do not think it can be: once the call returns, create-with-mode and create-then-chmod are indistinguishable by stat, and observing the intermediate state means racing the writer, so that property rests on the open flags. And I have not run the agent against a live instance, so the verification here is the unit tests plus a build; TestWriteTemplateToFileAppliesConfiguredPermission drives the same WriteTemplateToFile path the agent uses per render, but not the surrounding fetch-and-render loop. I can add an e2e/agent case if you want one, though that suite needs a composed instance and currently only covers certificates.


Rendered secret templates were written with a bare os.Create, so they landed 0644 under a
normal umask with no way to change it. The execute hook is not a workaround for this: it is
guarded by `if !firstRun`, so a chmod there is skipped on the very first render and the file
stays world-readable until an etag change forces a re-render.

Add a `permission` field to the template config and to the file sink config, spelled to match
the one certificates already accept. The access token sink had the same hardcoded 0644 on
higher-value content, so it is covered too.

Values are validated while the agent config is parsed, so a typo aborts startup instead of
silently falling back to a looser mode at the first render.

Defaults are unchanged. An unset permission keeps the umask-derived mode for templates and
0644 for sinks, so no existing deployment changes behaviour on upgrade.

Files are opened with the target mode rather than created at 0666 and chmod'ed afterwards,
which would leave a window where the path is world-readable. Permissions are checked at
open(2), so a descriptor obtained during that window would survive the chmod. The chmod is
still applied, to restore bits the umask stripped and to tighten a file that already existed.

The octal parser is promoted out of WriteCertificateFiles so there is one implementation
rather than two. It now rejects modes above 0777, which the certificate-only version accepted,
and logs a warning on a malformed value instead of silently falling back to 0600.

Fixes: Infisical#339
Signed-off-by: Alexander Chernov <alexander@chernov.it>
@infisical-cla-app

infisical-cla-app Bot commented Jul 30, 2026

Copy link
Copy Markdown

CLA satisfied. All contributors have signed the current CLA. The cla/signed check is passing.

Comment thread packages/cmd/agent.go Outdated
Comment on lines +831 to +839
outputFile, err := os.OpenFile(outputPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, createMode)
if err != nil {
return err
}
defer outputFile.Close()

if mode != nil {
if err := outputFile.Chmod(*mode); err != nil {
return fmt.Errorf("unable to set permission %#o on %s: %w", *mode, outputPath, err)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Truncation precedes permission validation

When a configured sink or template points to an existing file that the agent can write but does not own, OpenFile truncates the file before Chmod fails, leaving the token or rendered secret empty instead of preserving the last valid content.

Knowledge Base Used: Agent daemon (infisical agent)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

verify against the latest code

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The concern is resolved in the current code. The original diff I reviewed used O_TRUNC on open, which truncated before Chmod. The updated code removes that flag:

outputFile, err := os.OpenFile(outputPath, os.O_WRONLY|os.O_CREATE, createMode)

Chmod runs first, and only on success does Truncate(0) execute. The PR even documents this explicitly in the function comment:

"The file is deliberately opened without O_TRUNC and truncated only once the mode is known to be correct. Truncating first would destroy the previous content before a chmod failure could be reported..."

My original comment no longer applies.

Tip: You can customize Greptile's behavior for this repo with .greptile/rules.md and .greptile/config.json.

@greptile-apps

greptile-apps Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

Adds configurable octal permissions for agent token sinks and rendered templates.

  • Validates sink and template permission strings during configuration parsing.
  • Applies configured modes when creating or updating output files.
  • Centralizes file-mode parsing and reuses it for certificate outputs.
  • Adds Unix-focused tests for parsing, configuration validation, and file modes.

Confidence Score: 4/5

This PR should not merge until configured writes preserve the existing token or template when applying the requested mode fails.

Opening with O_TRUNC destroys the prior output before Chmod can fail, so a writable file owned by another UID is left empty rather than retaining its last valid secret content.

Files Needing Attention: packages/cmd/agent.go

Important Files Changed

Filename Overview
packages/cmd/agent.go Adds permission parsing, validation, and application, but can erase an existing output when chmod fails after truncation.
packages/cmd/agent_file_permission_test.go Thoroughly covers normal permission parsing and writes, but omits the writable-yet-not-owned existing-file failure case.
agent-config.yaml Updates the example configuration with valid 0600 permissions for sinks and templates.

Reviews (1): Last reviewed commit: "feat(agent): make output file permission..." | Re-trigger Greptile

alekc added 2 commits July 30, 2026 20:49
Truncating before the chmod destroyed the old content when the destination was writable but not
owned by the agent, so the file is now truncated only once the mode is known to be correct.

Refs: Infisical#342
Signed-off-by: Alexander Chernov <alexander@chernov.it>
Signed-off-by: Alexander Chernov <alexander@chernov.it>
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.

Agent renders secret templates world-readable (0644) with no way to set the mode

1 participant