Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions agent-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ sinks:
- type: "file"
config:
path: "access-token"
# Optional octal mode for the written token. Defaults to 0644.
permission: "0600"
templates:
- template-content: |
{{- with secret "202f04d7-e4cb-43d4-a292-e893712d61fc" "dev" "/" }}
Expand All @@ -20,6 +22,9 @@ templates:
destination-path: my-dot-env-0.env
config:
polling-interval: 60s
# Optional octal mode for the rendered file. When omitted the mode is
# left to the process umask, which usually means 0644.
permission: "0600"
execute:
command: docker-compose -f docker-compose.prod.yml down && docker-compose -f docker-compose.prod.yml up -d

Expand Down
131 changes: 120 additions & 11 deletions packages/cmd/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,8 @@ type Sink struct {
}

type SinkDetails struct {
Path string `yaml:"path"`
Path string `yaml:"path"`
Permission string `yaml:"permission"` // Octal file mode for the written token, e.g. "0600"
}

type Template struct {
Expand All @@ -190,6 +191,7 @@ type Template struct {

Config struct { // Configurations for the template
PollingInterval string `yaml:"polling-interval"` // How often to poll for changes in the secret
Permission string `yaml:"permission"` // Octal file mode for the rendered file, e.g. "0600"
Execute struct {
Command string `yaml:"command"` // Command to execute once the template has been rendered
Timeout int64 `yaml:"timeout"` // Timeout for the command
Expand Down Expand Up @@ -785,14 +787,64 @@ func FileExists(filepath string) bool {
return !info.IsDir()
}

// WriteToFile writes data to the specified file path.
func WriteBytesToFile(data *bytes.Buffer, outputPath string) error {
outputFile, err := os.Create(outputPath)
// parseFileMode parses an octal file-mode string from the agent config, such
// as "0600". An empty string yields a nil mode, which callers treat as "keep
// the existing behaviour" rather than forcing a permission on the file.
// Modes above 0777 are rejected so that setuid, setgid and the sticky bit
// cannot be set on a file holding secret material.
func parseFileMode(permission string) (*os.FileMode, error) {
if permission == "" {
return nil, nil
}

parsed, err := strconv.ParseUint(permission, 8, 32)
if err != nil {
return nil, fmt.Errorf("invalid file permission %q: expected an octal mode such as \"0600\"", permission)
}

if parsed > 0777 {
return nil, fmt.Errorf("invalid file permission %q: must not exceed \"0777\"", permission)
}

mode := os.FileMode(parsed)
return &mode, nil
}

// chmodFile is indirected so the chmod failure path can be covered by a test.
// fchmod cannot be made to fail portably from an unprivileged process, and the
// behaviour on failure matters enough to be worth pinning down.
var chmodFile = (*os.File).Chmod

// WriteBytesToFile writes data to the specified file path, creating it with the
// configured mode rather than os.Create's 0666.
//
// A nil mode preserves the historical behaviour of leaving the mode to the
// umask.
func WriteBytesToFile(data *bytes.Buffer, outputPath string, mode *os.FileMode) error {
createMode := os.FileMode(0666)
if mode != nil {
createMode = *mode
}

outputFile, err := os.OpenFile(outputPath, os.O_WRONLY|os.O_CREATE, createMode)
if err != nil {
return err
}
defer outputFile.Close()

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

// Without O_TRUNC this is what stops a shorter render from leaving
// trailing bytes of the previous one behind. Truncating here avoids a situation
// where we cannot write to file due to ownership, yet we truncate it
if err := outputFile.Truncate(0); err != nil {
return fmt.Errorf("unable to truncate %s: %w", outputPath, err)
}

_, err = outputFile.Write(data.Bytes())
return err
}
Expand Down Expand Up @@ -878,9 +930,33 @@ func parseAgentConfigWithMode(configFile []byte, isCertManagerMode bool) (*Confi
return nil, err
}

if err := validateFilePermissions(&rawConfig); err != nil {
return nil, err
}

return &rawConfig, nil
}

// validateFilePermissions rejects malformed permission values while the config
// is being parsed. Catching a typo here fails the agent at startup instead of
// silently falling back to a looser mode at the first render, which would be
// invisible until someone stats the file.
func validateFilePermissions(config *Config) error {
for i, sink := range config.Sinks {
if _, err := parseFileMode(sink.Config.Permission); err != nil {
return fmt.Errorf("sinks[%d].config.permission: %w", i, err)
}
}

for i, template := range config.Templates {
if _, err := parseFileMode(template.Config.Permission); err != nil {
return fmt.Errorf("templates[%d].config.permission: %w", i, err)
}
}

return nil
}

type secretArguments struct {
IsRecursive bool `json:"recursive"`
ShouldExpandSecretReferences *bool `json:"expandSecretReferences,omitempty"`
Expand Down Expand Up @@ -1804,11 +1880,29 @@ func (tm *AgentManager) ManageTokenLifecycle() {
func (tm *AgentManager) WriteTokenToFiles() {
token := tm.GetToken()

writeSink := func(path string, mode *os.FileMode) error {
if mode == nil {
// Historical behaviour: 0644 is applied only when the file is
// created, so an existing sink keeps whatever mode it already has.
return os.WriteFile(path, []byte(token), 0644)
}
return WriteBytesToFile(bytes.NewBufferString(token), path, mode)
}

for _, sinkFile := range tm.filePaths {
if sinkFile.Type == "file" {
err := ioutil.WriteFile(sinkFile.Config.Path, []byte(token), 0644)
// Already validated when the agent config was parsed, so an error
// here is not reachable in practice; fall back to the historical
// mode rather than dropping the token.
mode, err := parseFileMode(sinkFile.Config.Permission)
if err != nil {
log.Warn().Msgf("sink '%s': %v. Falling back to the default file mode", sinkFile.Config.Path, err)
mode = nil
}

if err := writeSink(sinkFile.Config.Path, mode); err != nil {
log.Error().Msgf("unable to write file sink to path '%s' because %v", sinkFile.Config.Path, err)
continue
}

log.Info().Msgf("new access token saved to file at path '%s'", sinkFile.Config.Path)
Expand Down Expand Up @@ -1838,7 +1932,16 @@ func (tm *AgentManager) FetchTokenFromFiles() string {
}

func (tm *AgentManager) WriteTemplateToFile(bytes *bytes.Buffer, template *Template, templateId int) {
if err := WriteBytesToFile(bytes, template.DestinationPath); err != nil {
// Already validated when the agent config was parsed, so an error here is
// not reachable in practice; fall back to the umask default rather than
// dropping the render.
mode, err := parseFileMode(template.Config.Permission)
if err != nil {
log.Warn().Msgf("template engine: %v. Falling back to the default file mode", err)
mode = nil
}

if err := WriteBytesToFile(bytes, template.DestinationPath, mode); err != nil {
log.Error().Msgf("template engine: unable to write secrets to path because %s. Will try again on next cycle", err)
return
}
Expand Down Expand Up @@ -2673,13 +2776,19 @@ func (tm *AgentManager) handleFailedCertificateRequest(certificateId int, errorM
}

func (tm *AgentManager) WriteCertificateFiles(certificate *AgentCertificateConfig, response *api.CertificateResponse) error {
// Certificates keep their long-standing 0600 default, including when the
// configured value is malformed. The warning is new: previously a typo was
// silently swallowed.
getFilePermission := func(permission string) os.FileMode {
if permission != "" {
if perms, err := strconv.ParseInt(permission, 8, 32); err == nil {
return os.FileMode(perms)
}
mode, err := parseFileMode(permission)
if err != nil {
log.Warn().Msgf("certificate file permission: %v. Falling back to 0600", err)
return os.FileMode(0600)
}
if mode == nil {
return os.FileMode(0600)
}
return os.FileMode(0600)
return *mode
}

privateKeyPath := certificate.FileConfig.PrivateKey.Path
Expand Down
Loading