Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Secrets Rotation: only write secret files if changed #432

Merged
merged 2 commits into from
Feb 7, 2022
Merged
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
1 change: 1 addition & 0 deletions pkg/log/messages/info_messages.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,4 @@ const CSPFK014I string = "CSPFK014I Authenticator setting %s provided by %s"
const CSPFK015I string = "CSPFK015I DAP/Conjur Secrets pushed to shared volume successfully"
const CSPFK016I string = "CSPFK016I There are no secrets to be retrieved from Conjur"
const CSPFK017I string = "CSPFK017I Creating default file name for secret group '%s'"
const CSPFK018I string = "CSPFK018I No change in secret file, no secret files written"
72 changes: 69 additions & 3 deletions pkg/secrets/pushtofile/push_to_writer.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
package pushtofile

import (
"bytes"
"crypto/sha256"
"fmt"
"io"
"os"
"path/filepath"
"text/template"

"github.com/cyberark/conjur-authn-k8s-client/pkg/log"
"github.com/cyberark/secrets-provider-for-k8s/pkg/log/messages"
)

// templateData describes the form in which data is presented to push-to-file templates
Expand All @@ -30,6 +35,13 @@ type openWriteCloserFunc func(
permissions os.FileMode,
) (io.WriteCloser, error)

type checksum []byte

// prevFileChecksums maps a secret group name to a sha256 checksum of the
// corresponding secret file content. This is used to detect changes in
// secret file content.
var prevFileChecksums = map[string]checksum{}

// openFileAsWriteCloser opens a file to write-to with some permissions.
func openFileAsWriteCloser(path string, permissions os.FileMode) (io.WriteCloser, error) {
dir := filepath.Dir(path)
Expand Down Expand Up @@ -65,7 +77,7 @@ func pushToWriter(
secretsMap[s.Alias] = s
}

t, err := template.New(groupName).Funcs(template.FuncMap{
tpl, err := template.New(groupName).Funcs(template.FuncMap{
// secret is a custom utility function for streamlined access to secret values.
// It panics for secrets aliases not specified on the group.
"secret": func(alias string) string {
Expand All @@ -85,8 +97,62 @@ func pushToWriter(
return err
}

return t.Execute(writer, templateData{
// Render the secret file content
tplData := templateData{
SecretsArray: groupSecrets,
SecretsMap: secretsMap,
})
}
fileContent, err := renderFile(tpl, tplData)
if err != nil {
return err
}

return writeContent(writer, fileContent, groupName)
}

func renderFile(tpl *template.Template, tplData templateData) (*bytes.Buffer, error) {
buf := &bytes.Buffer{}
err := tpl.Execute(buf, tplData)
return buf, err
}

func writeContent(writer io.Writer, fileContent *bytes.Buffer, groupName string) error {
if writer == io.Discard {
_, err := writer.Write(fileContent.Bytes())
return err
}

// Calculate a sha256 checksum on the content
checksum, _ := fileChecksum(fileContent)

// If file contents have changed, write the file and update checksum
if contentHasChanged(groupName, checksum) {
if _, err := writer.Write(fileContent.Bytes()); err != nil {
return err
}
prevFileChecksums[groupName] = checksum
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we need to log an INFO level message here that an individual file has been written?

Or do we assume that the user will deduce that the secret file has been written if we don't see the No change in secret files, no secret files written for this file, but we do see the DAP/Conjur Secrets pushed to shared volume successfully at the very end of the refresh cycle?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I don't think it's necessary to add a new log statement for this functionality. Nothing has changed in that scenario and the "Secrets pushed to shared volume" log at the end should be sufficient. I think it's more important to log if they aren't written since this could be a source of problems if, say, the secrets file was deleted.

Copy link
Contributor

Choose a reason for hiding this comment

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

Makes sense. The only weird aspect is that the logs will continually show "Secrets pushed to shared volume" when they really aren't (after the first write). But then again, we don't want to make the logs too chatty either.

} else {
log.Info(messages.CSPFK018I)
}
return nil
}

// fileChecksum calculates a checksum on the file content
func fileChecksum(buf *bytes.Buffer) (checksum, error) {
hash := sha256.New()
bufCopy := bytes.NewBuffer(buf.Bytes())
if _, err := io.Copy(hash, bufCopy); err != nil {
return nil, err
}
checksum := hash.Sum(nil)
return checksum, nil
}

func contentHasChanged(groupName string, fileChecksum checksum) bool {
if prevChecksum, exists := prevFileChecksums[groupName]; exists {
if bytes.Equal(fileChecksum, prevChecksum) {
return false
}
}
return true
}
55 changes: 55 additions & 0 deletions pkg/secrets/pushtofile/push_to_writer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -151,3 +151,58 @@ func Test_pushToWriter(t *testing.T) {
tc.Run(t)
}
}

func Test_pushToWriter_contentChanges(t *testing.T) {
t.Run("content changes", func(t *testing.T) {
// Call pushToWriter with a simple template and secret.
secrets := []*Secret{{Alias: "alias", Value: "secret value"}}
groupName := "group path"
template := `{{secret "alias"}}`

buf := new(bytes.Buffer)
err := pushToWriter(
buf,
groupName,
template,
secrets,
)
assert.NoError(t, err)
assert.Equal(t, "secret value", buf.String())

// Now clear the buffer and call pushToWriter again. Since the secret is the same,
// it should not update the buffer.
buf.Reset()
err = pushToWriter(
buf,
groupName,
template,
secrets,
)

assert.NoError(t, err)
assert.Zero(t, buf.Len())

// Now change the secret and call pushToWriter again. This time, the buffer should
// be updated because the secret has changed.
err = pushToWriter(
buf,
groupName,
template,
[]*Secret{{Alias: "alias", Value: "secret changed"}},
)
assert.NoError(t, err)
assert.Equal(t, "secret changed", buf.String())

// Repeat the test but this time change the template instead of the secret. The buffer should still
// be updated because the rendered output should be different.
buf.Reset()
err = pushToWriter(
buf,
groupName,
`- {{secret "alias"}}`,
[]*Secret{{Alias: "alias", Value: "secret changed"}},
)
assert.NoError(t, err)
assert.Equal(t, "- secret changed", buf.String())
})
}
20 changes: 10 additions & 10 deletions pkg/secrets/pushtofile/secret_group.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@ package pushtofile

import (
"fmt"
"github.com/cyberark/conjur-authn-k8s-client/pkg/log"
"github.com/cyberark/secrets-provider-for-k8s/pkg/log/messages"
"io/ioutil"
"os"
"path"
"path/filepath"
"sort"
"strings"

"github.com/cyberark/conjur-authn-k8s-client/pkg/log"
"github.com/cyberark/secrets-provider-for-k8s/pkg/log/messages"
)

const secretGroupPrefix = "conjur.org/conjur-secrets."
Expand Down Expand Up @@ -70,9 +71,8 @@ func (sg *SecretGroup) pushToFileWithDeps(
secrets []*Secret,
) (err error) {
// Make sure all the secret specs are accounted for
err = validateSecretsAgainstSpecs(secrets, sg.SecretSpecs)
if err != nil {
return
if err := validateSecretsAgainstSpecs(secrets, sg.SecretSpecs); err != nil {
return err
}

// Determine file template from
Expand All @@ -85,13 +85,13 @@ func (sg *SecretGroup) pushToFileWithDeps(
sg.SecretSpecs,
)
if err != nil {
return
return err
}

//// Open and push to file
wc, err := depOpenWriteCloser(sg.FilePath, sg.FilePermissions)
if err != nil {
return
return err
}
defer func() {
_ = wc.Close()
Expand All @@ -103,16 +103,16 @@ func (sg *SecretGroup) pushToFileWithDeps(
err = maskError
}
}()
pushToWriterErr := depPushToWriter(
err = depPushToWriter(
wc,
sg.Name,
fileTemplate,
secrets,
)
if pushToWriterErr != nil {
if err != nil {
err = maskError
}
return
return err
}

func (sg *SecretGroup) absoluteFilePath(secretsBasePath string) (string, error) {
Expand Down
Loading