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
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,23 @@ You can add your Valkey or Redis service to PMM either through the user interfac
Valkey-TLS
```

=== "With mutual TLS"

Add an instance that requires client certificate authentication:
```sh
pmm-admin add valkey \
Valkey-mTLS \
valkey-server.example.com:6379 \
--username=pmm \
--password=StrongPassword123! \
--tls \
--tls-ca=/path/to/ca.pem \
--tls-cert=/path/to/client-cert.pem \
--tls-key=/path/to/client-key.pem
```

`--tls-ca` already supplies the trust anchor, so a self-signed certificate does not need `--tls-skip-verify`. Add that flag only when a validation failure cannot be corrected — a SAN mismatch, for example — and only in development or testing: it disables server authentication entirely.

=== ":material-cog: Via inventory commands (Advanced)"
PMM also provides inventory commands for more granular control:

Expand Down
22 changes: 19 additions & 3 deletions managed/models/agent_model.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,14 @@ import (
// pmm-managed's PostgreSQL, qan-api's ClickHouse, and VictoriaMetrics.
type AgentType string

// Text file names carrying TLS material to pmm-agent. Exporter arguments reference them
// as {{ .TextFiles.<name> }}, so renaming one is a change to the agent wire protocol.
const (
TLSCaFileName = "tlsCa"
TLSCertFileName = "tlsCert"
TLSKeyFileName = "tlsKey"
)

const (
certificateFilePlaceholder = "certificateFilePlaceholder"
certificateKeyFilePlaceholder = "certificateKeyFilePlaceholder"
Expand Down Expand Up @@ -955,13 +963,13 @@ func (a Agent) Files() map[string]string { //nolint:gocognit
files := make(map[string]string)

if a.ValkeyOptions.SSLCa != "" {
files["tlsCa"] = a.ValkeyOptions.SSLCa
files[TLSCaFileName] = a.ValkeyOptions.SSLCa
}
if a.ValkeyOptions.SSLCert != "" {
files["tlsCert"] = a.ValkeyOptions.SSLCert
files[TLSCertFileName] = a.ValkeyOptions.SSLCert
}
if a.ValkeyOptions.SSLKey != "" {
files["tlsKey"] = a.ValkeyOptions.SSLKey
files[TLSKeyFileName] = a.ValkeyOptions.SSLKey
}

if len(files) != 0 {
Expand Down Expand Up @@ -996,6 +1004,14 @@ func (a Agent) TemplateDelimiters(svc *Service) *DelimiterPair {
if a.PostgreSQLOptions.SSLKey != "" {
templateParams = append(templateParams, a.PostgreSQLOptions.SSLKey)
}
case ValkeyServiceType:
// pmm-agent renders every text file's content as a template, so all three
// certificates have to be considered, not just the private key.
for _, s := range []string{a.ValkeyOptions.SSLCa, a.ValkeyOptions.SSLCert, a.ValkeyOptions.SSLKey} {
if s != "" {
templateParams = append(templateParams, s)
}
}
case ProxySQLServiceType:
case HAProxyServiceType:
case ExternalServiceType:
Expand Down
48 changes: 48 additions & 0 deletions managed/models/agent_model_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -286,7 +286,11 @@ func TestPostgresAgentTLS(t *testing.T) {
}

func TestValkey(t *testing.T) {
t.Parallel()

t.Run("Redis DSN", func(t *testing.T) {
t.Parallel()

agent := &models.Agent{
Username: new("username"),
Password: new("s3cur3 p@$$w0r4."),
Expand All @@ -305,6 +309,8 @@ func TestValkey(t *testing.T) {
})

t.Run("Valkey DSN with TLS", func(t *testing.T) {
t.Parallel()

agent := &models.Agent{
Username: new("username"),
Password: new("s3cur3 p@$$w0r4."),
Expand All @@ -326,6 +332,48 @@ func TestValkey(t *testing.T) {

require.Equal(t, expected, agent.DSN(service, models.DSNParams{DialTimeout: time.Second, Database: "database"}, nil, nil))
})

t.Run("Files", func(t *testing.T) {
t.Parallel()

for name, tc := range map[string]struct {
options models.ValkeyOptions
expected map[string]string
}{
"all": {models.ValkeyOptions{SSLCa: "aa", SSLCert: "bb", SSLKey: "cc"}, map[string]string{"tlsCa": "aa", "tlsCert": "bb", "tlsKey": "cc"}},
"ca": {models.ValkeyOptions{SSLCa: "aa"}, map[string]string{"tlsCa": "aa"}},
"pair": {models.ValkeyOptions{SSLCert: "bb", SSLKey: "cc"}, map[string]string{"tlsCert": "bb", "tlsKey": "cc"}},
"none": {models.ValkeyOptions{}, nil},
} {
t.Run(name, func(t *testing.T) {
t.Parallel()

agent := models.Agent{AgentType: models.ValkeyExporterType, ValkeyOptions: tc.options}

require.Equal(t, tc.expected, agent.Files())
})
}
})

t.Run("TemplateDelimiters avoid certificate content", func(t *testing.T) {
t.Parallel()

service := &models.Service{ServiceType: models.ValkeyServiceType, Address: new("1.2.3.4")}

for name, options := range map[string]models.ValkeyOptions{
"ca": {SSLCa: "aa {{ bb"},
"cert": {SSLCert: "aa {{ bb"},
"key": {SSLKey: "aa {{ bb"},
} {
t.Run(name, func(t *testing.T) {
t.Parallel()

agent := models.Agent{AgentType: models.ValkeyExporterType, ValkeyOptions: options}

require.Equal(t, &models.DelimiterPair{Left: "[[", Right: "]]"}, agent.TemplateDelimiters(service))
})
}
})
}

func TestPostgresWithSocket(t *testing.T) {
Expand Down
3 changes: 3 additions & 0 deletions managed/services/agents/agents.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,9 @@ func redactWords(agent *models.Agent) []string {
if s := agent.PostgreSQLOptions.SSLKey; s != "" {
words = append(words, s)
}
if s := agent.ValkeyOptions.SSLKey; s != "" {
words = append(words, s)
}

return words
}
Expand Down
26 changes: 23 additions & 3 deletions managed/services/agents/valkey.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,33 @@ func valkeyExporterConfig(node *models.Node, service *models.Service, exporter *
args = append(args, "--web.telemetry-path="+exporter.ExporterOptions.MetricsPath)
}

textFiles := exporter.Files()
if exporter.TLS {
if exporter.TLSSkipVerify {
args = append(args, "--skip-tls-verification")
}

// The flag names come from oliver006/redis_exporter, shipped as valkey_exporter;
// all four have been stable since v1.72.1, the build the first Valkey release used.
tlsFileFlags := []struct{ file, flag string }{
{models.TLSCaFileName, "--tls-ca-cert-file"},
{models.TLSCertFileName, "--tls-client-cert-file"},
{models.TLSKeyFileName, "--tls-client-key-file"},
}
for _, f := range tlsFileFlags {
if _, ok := textFiles[f.file]; ok {
args = append(args, f.flag+"="+tdp.Left+" .TextFiles."+f.file+" "+tdp.Right)
}
}
}

dsnParams := models.DSNParams{}
connectionTimeout := exporter.EffectiveDialTimeout()

args = append(args, "--redis.addr="+exporter.DSN(service, dsnParams, nil, pmmAgentVersion))
args = append(args, "--redis.addr="+exporter.DSN(service, dsnParams, tdp, pmmAgentVersion))
args = append(args, "--connection-timeout="+connectionTimeout.String())
// valkey_exporter parses flags with the stdlib flag package, which rejects --log.level
// and has no fatal level (PMM-15201).
// and has no fatal level.
args = withLogLevelFlag(args, "--log-level", exporter.LogLevel, pmmAgentVersion, false)
sort.Strings(args)

Expand All @@ -55,7 +75,7 @@ func valkeyExporterConfig(node *models.Node, service *models.Service, exporter *
TemplateLeftDelim: tdp.Left,
TemplateRightDelim: tdp.Right,
Args: args,
TextFiles: exporter.Files(),
TextFiles: textFiles,
}
if redactMode != exposeSecrets {
res.RedactWords = redactWords(exporter)
Expand Down
Loading
Loading