diff --git a/api/cmd/portainer/main.go b/api/cmd/portainer/main.go index 23c2f1274..6ad878db6 100644 --- a/api/cmd/portainer/main.go +++ b/api/cmd/portainer/main.go @@ -307,8 +307,19 @@ func initKeyPair(fileService portainer.FileService, signatureService portainer.D return generateAndStoreKeyPair(fileService, signatureService) } +// dbSecretPath build the path to the file that contains the db encryption +// secret. Normally in Docker this is built from the static path inside +// /run/portainer for example: /run/portainer/ but for ease of +// use outside Docker it also accepts an absolute path +func dbSecretPath(keyFilenameFlag string) string { + if path.IsAbs(keyFilenameFlag) { + return keyFilenameFlag + } + return path.Join("/run/portainer", keyFilenameFlag) +} + func loadEncryptionSecretKey(keyfilename string) []byte { - content, err := os.ReadFile(path.Join("/run/secrets", keyfilename)) + content, err := os.ReadFile(keyfilename) if err != nil { if os.IsNotExist(err) { log.Info().Str("filename", keyfilename).Msg("encryption key file not present") @@ -320,6 +331,7 @@ func loadEncryptionSecretKey(keyfilename string) []byte { } // return a 32 byte hash of the secret (required for AES) + // fips compliant version of this is not implemented in -ce hash := sha256.Sum256(content) return hash[:] @@ -348,7 +360,7 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server { fips.InitFIPS(false) fileService := initFileService(*flags.Data) - encryptionKey := loadEncryptionSecretKey(*flags.SecretKeyName) + encryptionKey := loadEncryptionSecretKey(dbSecretPath(*flags.SecretKeyName)) if encryptionKey == nil { log.Info().Msg("proceeding without encryption key") } diff --git a/api/cmd/portainer/main_test.go b/api/cmd/portainer/main_test.go new file mode 100644 index 000000000..da271949f --- /dev/null +++ b/api/cmd/portainer/main_test.go @@ -0,0 +1,57 @@ +package main + +import ( + "os" + "path" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +const secretFileName = "secret.txt" + +func createPasswordFile(t *testing.T, secretPath, password string) string { + err := os.WriteFile(secretPath, []byte(password), 0600) + require.NoError(t, err) + return secretPath +} + +func TestLoadEncryptionSecretKey(t *testing.T) { + tempDir := t.TempDir() + secretPath := path.Join(tempDir, secretFileName) + + // first pointing to file that does not exist, gives nil hash (no encryption) + encryptionKey := loadEncryptionSecretKey(secretPath) + require.Nil(t, encryptionKey) + + // point to a directory instead of a file + encryptionKey = loadEncryptionSecretKey(tempDir) + require.Nil(t, encryptionKey) + + password := "portainer@1234" + createPasswordFile(t, secretPath, password) + + encryptionKey = loadEncryptionSecretKey(secretPath) + require.NotNil(t, encryptionKey) + // should be 32 bytes for aes256 encryption + require.Len(t, encryptionKey, 32) +} + +func TestDBSecretPath(t *testing.T) { + tests := []struct { + keyFilenameFlag string + expected string + }{ + {keyFilenameFlag: "secret.txt", expected: "/run/portainer/secret.txt"}, + {keyFilenameFlag: "/tmp/secret.txt", expected: "/tmp/secret.txt"}, + {keyFilenameFlag: "/run/portainer/secret.txt", expected: "/run/portainer/secret.txt"}, + {keyFilenameFlag: "./secret.txt", expected: "/run/portainer/secret.txt"}, + {keyFilenameFlag: "../secret.txt", expected: "/run/secret.txt"}, + {keyFilenameFlag: "foo/bar/secret.txt", expected: "/run/portainer/foo/bar/secret.txt"}, + } + + for _, test := range tests { + assert.Equal(t, test.expected, dbSecretPath(test.keyFilenameFlag)) + } +}