Skip to content
Closed
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
28 changes: 25 additions & 3 deletions agent/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,8 @@ type Config struct {

WindowConnectedTime time.Duration `yaml:"window-connected-time"`

Setup Setup `yaml:"-"`
Setup Setup `yaml:"-"`
Encryption Encryption `yaml:"-"`
}

// ConfigFileDoesNotExistError error is returned from Get method if configuration file is expected,
Expand Down Expand Up @@ -335,7 +336,7 @@ func get(args []string, cfg *Config, l *logrus.Entry) (string, error) { //nolint
return configFileF, err
}
l.Infof("Loading configuration file %s.", configFileF)
fileCfg, err := loadFromFile(configFileF)
fileCfg, err := loadFromFile(configFileF, &cfg.Encryption)
if err != nil {
return configFileF, err
}
Expand Down Expand Up @@ -365,6 +366,10 @@ func Application(cfg *Config) (*kingpin.Application, *string) {

configFileF := app.Flag("config-file", "Configuration file path [PMM_AGENT_CONFIG_FILE]").
Envar("PMM_AGENT_CONFIG_FILE").PlaceHolder("</path/to/pmm-agent.yaml>").String()
app.Flag("config-file-key-file", "Path to the key file used to encrypt/decrypt the configuration file").
Envar("PMM_AGENT_CONFIG_FILE_KEY_FILE").StringVar(&cfg.Encryption.KeyFile)
app.Flag("config-file-key-password", "Password for the key file (if required)").
Envar("PMM_AGENT_CONFIG_FILE_KEY_PASSWORD").StringVar(&cfg.Encryption.KeyFilePassword)

app.Flag("id", "ID of this pmm-agent [PMM_AGENT_ID]").
Envar("PMM_AGENT_ID").StringVar(&cfg.ID)
Expand Down Expand Up @@ -526,7 +531,7 @@ func Application(cfg *Config) (*kingpin.Application, *string) {
// As a special case, if file does not exist, it returns ConfigFileDoesNotExistError.
// Other errors are returned if file exists, but configuration can't be loaded due to permission problems,
// YAML parsing problems, etc.
func loadFromFile(path string) (*Config, error) {
func loadFromFile(path string, enc *Encryption) (*Config, error) {
if _, err := os.Stat(path); errors.Is(err, fs.ErrNotExist) {
return nil, ConfigFileDoesNotExistError(path)
}
Expand All @@ -535,6 +540,15 @@ func loadFromFile(path string) (*Config, error) {
if err != nil {
return nil, err
}

encryptionEnabled := enc != nil && len(enc.KeyFile) > 0 && len(b) > 0
if encryptionEnabled {
b, err = enc.Decrypt(b)
if err != nil {
return nil, err
}
}

cfg := &Config{}
if err = yaml.Unmarshal(b, cfg); err != nil { //nolint:musttag // false positive
return nil, err
Expand All @@ -556,6 +570,14 @@ func SaveToFile(path string, cfg *Config, comment string) error {
}
res = append(res, "---\n"...)
res = append(res, b...)
encryptionEnabled := cfg != nil && len(cfg.Encryption.KeyFile) > 0
if encryptionEnabled {
res, err = cfg.Encryption.Encrypt(res)
if err != nil {
return err
}
}

return os.WriteFile(path, res, 0o640) //nolint:gosec
}

Expand Down
8 changes: 4 additions & 4 deletions agent/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,13 +52,13 @@ func TestLoadFromFile(t *testing.T) {
name := writeConfig(t, &Config{ID: "agent-id"})
t.Cleanup(func() { removeConfig(t, name) })

cfg, err := loadFromFile(name)
cfg, err := loadFromFile(name, nil)
require.NoError(t, err)
assert.Equal(t, &Config{ID: "agent-id"}, cfg)
})

t.Run("NotExist", func(t *testing.T) {
cfg, err := loadFromFile("not-exist.yaml")
cfg, err := loadFromFile("not-exist.yaml", nil)
assert.Equal(t, ConfigFileDoesNotExistError("not-exist.yaml"), err)
assert.Nil(t, cfg)
})
Expand All @@ -68,7 +68,7 @@ func TestLoadFromFile(t *testing.T) {
require.NoError(t, os.Chmod(name, 0o000))
t.Cleanup(func() { removeConfig(t, name) })

cfg, err := loadFromFile(name)
cfg, err := loadFromFile(name, nil)
require.IsType(t, (*os.PathError)(nil), err)
assert.Equal(t, "open", err.(*os.PathError).Op) //nolint:errorlint
require.EqualError(t, err.(*os.PathError).Err, "permission denied") //nolint:errorlint
Expand All @@ -80,7 +80,7 @@ func TestLoadFromFile(t *testing.T) {
require.NoError(t, os.WriteFile(name, []byte(`not YAML`), 0o666)) //nolint:gosec
t.Cleanup(func() { removeConfig(t, name) })

cfg, err := loadFromFile(name)
cfg, err := loadFromFile(name, nil)
require.IsType(t, (*yaml.TypeError)(nil), err)
require.EqualError(t, err, "yaml: unmarshal errors:\n line 1: cannot unmarshal !!str `not YAML` into config.Config")
assert.Nil(t, cfg)
Expand Down
122 changes: 122 additions & 0 deletions agent/config/encryption.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
package config

import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"encoding/pem"
"io"
"os"

"github.com/pkg/errors"
"github.com/youmark/pkcs8"
)

type Encryption struct {
KeyFile string
KeyFilePassword string
}

const gcmNonceSize = 12

func (enc Encryption) Encrypt(plain []byte) ([]byte, error) {
priv, err := enc.readKeyFile()
if err != nil {
return nil, errors.Wrap(err, "unable to get RSA key from KeyFile")
}

aesKey := make([]byte, 32)
if _, err := io.ReadFull(rand.Reader, aesKey); err != nil {
return nil, errors.Wrap(err, "unable to generate AES key")
}

block, err := aes.NewCipher(aesKey)
if err != nil {
return nil, errors.Wrap(err, "unable to init AES")
}
gcm, err := cipher.NewGCMWithNonceSize(block, gcmNonceSize)
if err != nil {
return nil, errors.Wrap(err, "unable to init GCM")
}
nonce := make([]byte, gcmNonceSize)
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
return nil, errors.Wrap(err, "unable to generate nonce")
}

ciphertext := gcm.Seal(nil, nonce, plain, nil)

wrappedKey, err := rsa.EncryptOAEP(sha256.New(), rand.Reader, &priv.PublicKey, aesKey, nil)
if err != nil {
return nil, errors.Wrap(err, "unable to RSA-wrap AES key")
}

out := make([]byte, 0, len(wrappedKey)+len(nonce)+len(ciphertext))
out = append(out, wrappedKey...)
out = append(out, nonce...)
out = append(out, ciphertext...)
return out, nil
}

func (enc Encryption) Decrypt(in []byte) ([]byte, error) {
priv, err := enc.readKeyFile()
if err != nil {
return nil, errors.Wrap(err, "unable to get RSA key from KeyFile")
}

k := priv.PublicKey.Size()
if len(in) < k+gcmNonceSize+1 {
return nil, errors.New("ciphertext too short")
}

wrappedKey := in[:k]
nonce := in[k : k+gcmNonceSize]
ciphertext := in[k+gcmNonceSize:]

aesKey, err := rsa.DecryptOAEP(sha256.New(), rand.Reader, priv, wrappedKey, nil)
if err != nil {
return nil, errors.Wrap(err, "unable to RSA-unwrap AES key")
}
if len(aesKey) != 32 {
return nil, errors.Errorf("unexpected AES key length: %d", len(aesKey))
}

block, err := aes.NewCipher(aesKey)
if err != nil {
return nil, errors.Wrap(err, "unable to init AES")
}
gcm, err := cipher.NewGCMWithNonceSize(block, gcmNonceSize)
if err != nil {
return nil, errors.Wrap(err, "unable to init GCM")
}

plain, err := gcm.Open(nil, nonce, ciphertext, nil)
if err != nil {
return nil, errors.Wrap(err, "unable to decrypt (wrong key or data tampered)")
}
return plain, nil
}

func (enc Encryption) readKeyFile() (*rsa.PrivateKey, error) {
f, err := os.ReadFile(enc.KeyFile)
if err != nil {
return nil, errors.Wrap(err, "unable to read KeyFile")
}

block, _ := pem.Decode(f)
if block == nil {
return nil, errors.New("no valid private key found in a KeyFile")
}

k, err := pkcs8.ParsePKCS8PrivateKey(block.Bytes, []byte(enc.KeyFilePassword))
if err != nil {
return nil, errors.Wrap(err, "unable to parse private key")
}

rsaKey, ok := k.(*rsa.PrivateKey)
if !ok {
return nil, errors.New("private key is not RSA")
}
return rsaKey, nil
}
116 changes: 116 additions & 0 deletions agent/config/encryption_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
package config

import (
"os"
"path/filepath"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

const rsaKey = `
-----BEGIN PRIVATE KEY-----
MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAL7XQZoBKba6RYXp
WsmX8kaeX4OBsNyqUrho3eh85jdEpfAYwzlKWNy3aboDre85ugwMEt5yMFOXL96r
9KnFrp4KpEacfJJdcoAPncAIslb0anwhhXOoc9ZIgz7AyJMKUN7PQSC4VpSP/86w
CShq5cEyRPbs4CwnQ5yYtlqCWQi1AgMBAAECgYA3EuXSrN0953mi0JorrVb0vEWy
LN4+gETJBTJtIoZJkt0UcgD86pDEeYXgcaljbVRcn6teWLPLm8jryNIdoHfoknIB
crf6vemmlP80Lpw2cdg46Q9lcleGTwJGOd+R2QSWJLV7kPrhhR+wIw6m7TDHvhGU
yx9AY4GrAMSrf4wTbQJBAN+3KuNssRBRZXKBDMuCAwgr8hSTNmjivRGmmATv3MGF
cCey/PbvQQ1jPRViSUysmFumOTE59GlcmB6TQQeUXF8CQQDaYZWIuGuAenI4pz+w
BR8JyJs5N52/YmsxTe2XShqksiiyvaHxNJ1mvSVEorPqEwfMwwFLI+cq0rh7YwZd
QLNrAkAV5QlPhL23iR/SmwqziB/f1t00Ykv66+XxKkrKgOcsEXEukXfsevH063d4
9kuSM3odziDezns7LJK+u06r/TslAkEAjowiStNuwLestVRe2ywMnZtHz2qBWvsI
U2+1xgqGJ7lvnXTxL3yTvgt7NzkpTYLMlZk4z+6Ip8hSyZ/S+K4SLwJAPO8sd5U8
mfvGLl15b1BI4o86X4r+HC4Nwb33i4eVBM06YBYbxSKUu9E9lWYiU1wCgPA7+T75
TF1yxL9OKxrMpQ==
-----END PRIVATE KEY-----
`

const rsaPasswordKey = `
-----BEGIN ENCRYPTED PRIVATE KEY-----
MIIC5TBfBgkqhkiG9w0BBQ0wUjAxBgkqhkiG9w0BBQwwJAQQ1vE1Ke5aqPfS+DEA
U0f79QICCAAwDAYIKoZIhvcNAgkFADAdBglghkgBZQMEASoEEKoYunBmlc2MEbT2
aaTRrNcEggKAKAmW5WL80flLmh+llCu4claeq5PT3DfrhUvL0UkdXDVV70aIfOMr
C7d28usIcfbguEtyPA72oWraNpot0u8z6SxVEZ0lFz4tk4eG8II1vLuzSXsswM0q
JKBGwbkwPVFIq21pK1vmfvWpA6gTTd7QU4YKInq1eXuoxHkdDC37ryXEUx/tEd+s
Gd54Zs1QXh2k0jSzTaOLlUaez9ADT1Nk9pS9Fj2aUSns7xXVXKYMYxpEYkUfd7xK
mowC0q0L/av9Xoj+EI5H0f9CdbufMpCe9GPAPShEEkV2feZsuigneAAYiIyAOMUh
T954t9M/rsSN5jAAZ/syaC6bDblx+nL6hDUcQjLyUJah7GRGwt5xexgMtfZccS8n
dp0DNG1gJjhK1QaP0BNBslMGlrbEoIkMn3MbmKVwnmKK/AM9kqalJV45RAogXpTG
lMhJ2bTpgx0CfIzalVEWc3eo7E0tf+Q//OHOptCx/u/sjeO5ZScyYt4SCiRsPYbJ
MjRQd3w3rQj5dW8f7ewDOf1xjueJ6q9sNL0f1OXgmCwq6MUbA2EpOMorETuZaATy
pLhDt8QNTp75NfENGbR+Uk+CoezmxS6KKGEG6P7SAvXnBHY4TGPNQq+pxaWxu6PB
+AS+K3t64XElJT99IB6rGjjpODQRQhk3IiX7hsJpG3KsTi2/sG/ZUkHJ5p7SDiK8
/q7EECYgQoqkQ4CFXbF1+Nz0fVIkX3FfRgJtKeuxNnjsY/CXnb1PERendg+6U0sp
e60uBuuy8TnXdkC3MIZyuc0iuZkw+XXdgZctv3bcAmkMDIcm0wNX5248N6tWcBUb
9j2emDUrENFV+8s8F+Mkj0mpoB+lre7Tww==
-----END ENCRYPTED PRIVATE KEY-----
`

func writeKey(t *testing.T, keyname, key string) string {
t.Helper()
path := filepath.Join(t.TempDir(), keyname)
require.NoError(t, os.WriteFile(path, []byte(key), 0o600))
return path
}

func TestEncryption(t *testing.T) {

t.Run("Encrypted", func(t *testing.T) {
key := writeKey(t, "key", rsaKey)
enc := Encryption{
KeyFile: key,
}
configfilef := writeConfig(t, &Config{ID: "agent-id", Encryption: enc})
cfg, err := loadFromFile(configfilef, &enc)
require.NoError(t, err)
assert.Equal(t, &Config{ID: "agent-id"}, cfg)

})

t.Run("EncryptedPassword", func(t *testing.T) {
key := writeKey(t, "key", rsaPasswordKey)
enc := Encryption{
KeyFile: key,
KeyFilePassword: "abcdefgh",
}
configfilef := writeConfig(t, &Config{ID: "agent-id", Encryption: enc})
cfg, err := loadFromFile(configfilef, &enc)
require.NoError(t, err)
assert.Equal(t, &Config{ID: "agent-id"}, cfg)

})

t.Run("EncryptedWrongPassword", func(t *testing.T) {
key := writeKey(t, "key", rsaPasswordKey)
configfilef := writeConfig(t, &Config{ID: "agent-id", Encryption: Encryption{
KeyFile: key,
KeyFilePassword: "abcdefgh",
}})

cfg, err := loadFromFile(configfilef, &Encryption{
KeyFile: key,
KeyFilePassword: "hgfedcba",
})
require.EqualError(t, err, "unable to get RSA key from KeyFile: unable to parse private key: pkcs8: incorrect password")
assert.Nil(t, cfg)
})

t.Run("EncryptedWrongKey", func(t *testing.T) {
key1 := writeKey(t, "key1", rsaPasswordKey)
key2 := writeKey(t, "key2", rsaKey)

configfilef := writeConfig(t, &Config{ID: "agent-id", Encryption: Encryption{
KeyFile: key2,
}})
cfg, err := loadFromFile(configfilef, &Encryption{
KeyFile: key1,
KeyFilePassword: "abcdefgh",
})
require.EqualError(t, err, "unable to RSA-unwrap AES key: crypto/rsa: decryption error")
assert.Nil(t, cfg)
})

}
Loading