Skip to content

Commit b00d2dd

Browse files
committed
Add target schema validation, http/https only
Add tls auth type support in multi-target mode Update README.md, examples/auth_modules.yml, tests Signed-off-by: pincher95 <[email protected]>
1 parent b9b80d9 commit b00d2dd

File tree

7 files changed

+264
-123
lines changed

7 files changed

+264
-123
lines changed

README.md

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ elasticsearch_exporter --help
5555
| Argument | Introduced in Version | Description | Default |
5656
| ----------------------- | --------------------- |---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| ----------- |
5757
| collector.clustersettings| 1.6.0 | If true, query stats for cluster settings (As of v1.6.0, this flag has replaced "es.cluster_settings"). | false |
58-
| es.uri | 1.0.2 | Address (host and port) of the Elasticsearch node we should connect to **when running in single-target mode**. Leave empty (the default) when you want to run the exporter only as a multi-target `/probe` endpoint. When basic auth is needed, specify as: `<proto>://<user>:<password>@<host>:<port>`. E.G., `http://admin:pass@localhost:9200`. Special characters in the user credentials need to be URL-encoded. | ״״ |
58+
| es.uri | 1.0.2 | Address (host and port) of the Elasticsearch node we should connect to **when running in single-target mode**. Leave empty (the default) when you want to run the exporter only as a multi-target `/probe` endpoint. When basic auth is needed, specify as: `<proto>://<user>:<password>@<host>:<port>`. E.G., `http://admin:pass@localhost:9200`. Special characters in the user credentials need to be URL-encoded. | "" |
5959
| es.all | 1.0.2 | If true, query stats for all nodes in the cluster, rather than just the node we connect to. | false |
6060
| es.indices | 1.0.2 | If true, query stats for all indices in the cluster. | false |
6161
| es.indices_settings | 1.0.4rc1 | If true, query settings stats for all indices in the cluster. | false |
@@ -120,10 +120,12 @@ From v2.X the exporter exposes `/probe` allowing one running instance to scrape
120120

121121
Supported `auth_module` types:
122122

123-
| type | YAML fields | Injected into request |
124-
| ---------- | ----------------------------------------------------------------- | ------------------------------------------------------------------ |
125-
| `userpass` | `userpass.username`, `userpass.password`, optional `options:` map | Sets HTTP basic-auth header, appends `options` as query parameters |
126-
| `apikey` | `apikey:` Base64 API-Key string, optional `options:` map | Adds `Authorization: ApiKey …` header, appends `options` |
123+
| type | YAML fields | Injected into request |
124+
| ---------- | ----------------------------------------------------------------- | ------------------------------------------------------------------------------------- |
125+
| `userpass` | `userpass.username`, `userpass.password`, optional `options:` map | Sets HTTP basic-auth header, appends `options` as query parameters |
126+
| `apikey` | `apikey:` Base64 API-Key string, optional `options:` map | Adds `Authorization: ApiKey …` header, appends `options` |
127+
| `aws` | `aws.region`, optional `aws.role_arn`, optional `options:` map | Uses AWS SigV4 signing transport for HTTP(S) requests, appends `options` |
128+
| `tls` | `tls.ca_file`, `tls.cert_file`, `tls.key_file` | Uses client certificate authentication via TLS; cannot be mixed with other auth types |
127129

128130
Example config:
129131

@@ -167,6 +169,12 @@ Prometheus scrape_config:
167169
replacement: exporter:9114
168170
```
169171
172+
Notes:
173+
- `/metrics` serves a single, process-wide registry and is intended for single-target mode.
174+
- `/probe` creates a fresh registry per scrape for the given `target` allowing multi-target scraping.
175+
- Any `options:` under an auth module will be appended as URL query parameters to the target URL.
176+
- The `tls` auth module (client certificate authentication) is intended for self‑managed Elasticsearch/OpenSearch deployments. Amazon OpenSearch Service typically authenticates at the domain edge with IAM/SigV4 and does not support client certificate authentication; use the `aws` auth module instead when scraping Amazon OpenSearch Service domains.
177+
170178
### Metrics
171179

172180
| Name | Type | Cardinality | Help |

config/config.go

Lines changed: 39 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ type UserPassConfig struct {
5757
// validate ensures every auth module has the required fields according to its type.
5858
func (c *Config) validate() error {
5959
for name, am := range c.AuthModules {
60+
// Validate fields based on auth type
6061
switch strings.ToLower(am.Type) {
6162
case "userpass":
6263
if am.UserPass == nil || am.UserPass.Username == "" || am.UserPass.Password == "" {
@@ -70,21 +71,52 @@ func (c *Config) validate() error {
7071
if am.AWS == nil || am.AWS.Region == "" {
7172
return fmt.Errorf("auth_module %s type aws requires region", name)
7273
}
74+
case "tls":
75+
// TLS auth type means client certificate authentication only (no other auth)
76+
if am.TLS == nil {
77+
return fmt.Errorf("auth_module %s type tls requires tls configuration section", name)
78+
}
79+
if am.TLS.CertFile == "" || am.TLS.KeyFile == "" {
80+
return fmt.Errorf("auth_module %s type tls requires cert_file and key_file for client certificate authentication", name)
81+
}
82+
// Validate that other auth fields are not set when using TLS auth type
83+
if am.UserPass != nil {
84+
return fmt.Errorf("auth_module %s type tls cannot have userpass configuration", name)
85+
}
86+
if am.APIKey != "" {
87+
return fmt.Errorf("auth_module %s type tls cannot have apikey", name)
88+
}
89+
if am.AWS != nil {
90+
return fmt.Errorf("auth_module %s type tls cannot have aws configuration", name)
91+
}
7392
default:
7493
return fmt.Errorf("auth_module %s has unsupported type %s", name, am.Type)
7594
}
7695

77-
// TLS validation (optional but if specified must be coherent)
96+
// Validate TLS configuration (optional for all auth types, provides transport security)
7897
if am.TLS != nil {
79-
if (am.TLS.CertFile == "") != (am.TLS.KeyFile == "") {
80-
return fmt.Errorf("auth_module %s tls requires both cert_file and key_file or neither", name)
98+
// For cert-based auth (type: tls), cert and key are required
99+
// For other auth types, TLS config is optional and used for transport security
100+
if strings.ToLower(am.Type) == "tls" {
101+
// Already validated above that cert and key are present
102+
} else {
103+
// For non-TLS auth types, if cert/key are provided, both must be present
104+
if (am.TLS.CertFile != "") != (am.TLS.KeyFile != "") {
105+
return fmt.Errorf("auth_module %s: if providing client certificate, both cert_file and key_file must be specified", name)
106+
}
81107
}
82-
for _, p := range []string{am.TLS.CAFile, am.TLS.CertFile, am.TLS.KeyFile} {
83-
if p == "" {
108+
109+
// Validate file accessibility
110+
for fileType, path := range map[string]string{
111+
"ca_file": am.TLS.CAFile,
112+
"cert_file": am.TLS.CertFile,
113+
"key_file": am.TLS.KeyFile,
114+
} {
115+
if path == "" {
84116
continue
85117
}
86-
if _, err := os.Stat(p); err != nil {
87-
return fmt.Errorf("auth_module %s tls file %s not accessible: %w", name, p, err)
118+
if _, err := os.Stat(path); err != nil {
119+
return fmt.Errorf("auth_module %s: %s '%s' not accessible: %w", name, fileType, path, err)
88120
}
89121
}
90122
}

config/config_test.go

Lines changed: 119 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,3 @@
1-
// Copyright The Prometheus Authors
2-
// Licensed under the Apache License, Version 2.0 (the "License");
3-
// you may not use this file except in compliance with the License.
4-
// You may obtain a copy of the License at
5-
//
6-
// http://www.apache.org/licenses/LICENSE-2.0
7-
//
8-
// Unless required by applicable law or agreed to in writing, software
9-
// distributed under the License is distributed on an "AS IS" BASIS,
10-
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11-
// See the License for the specific language governing permissions and
12-
// limitations under the License.
13-
141
package config
152

163
import (
@@ -27,91 +14,152 @@ func mustTempFile(t *testing.T) string {
2714
return f.Name()
2815
}
2916

30-
func TestLoadConfigTLSValid(t *testing.T) {
17+
// ---------------------------- Positive cases ----------------------------
18+
func TestLoadConfigPositiveVariants(t *testing.T) {
3119
ca := mustTempFile(t)
3220
cert := mustTempFile(t)
3321
key := mustTempFile(t)
34-
yaml := `auth_modules:
35-
secure:
22+
23+
positive := []struct {
24+
name string
25+
yaml string
26+
}{{
27+
"userpass",
28+
`auth_modules:
29+
basic:
3630
type: userpass
3731
userpass:
38-
username: foo
39-
password: bar
32+
username: u
33+
password: p`,
34+
}, {
35+
"userpass-with-tls",
36+
`auth_modules:
37+
basic:
38+
type: userpass
39+
userpass:
40+
username: u
41+
password: p
42+
tls:
43+
ca_file: ` + ca + `
44+
insecure_skip_verify: true`,
45+
}, {
46+
"apikey",
47+
`auth_modules:
48+
key:
49+
type: apikey
50+
apikey: ZXhhbXBsZQ==`,
51+
}, {
52+
"apikey-with-tls",
53+
`auth_modules:
54+
key:
55+
type: apikey
56+
apikey: ZXhhbXBsZQ==
4057
tls:
4158
ca_file: ` + ca + `
4259
cert_file: ` + cert + `
43-
key_file: ` + key + `
44-
`
45-
tmp, _ := os.CreateTemp(t.TempDir(), "cfg-*.yml")
46-
tmp.WriteString(yaml)
47-
tmp.Close()
48-
if _, err := LoadConfig(tmp.Name()); err != nil {
49-
t.Fatalf("expected config to load, got %v", err)
50-
}
51-
}
52-
53-
func TestLoadConfigTLSMissingKey(t *testing.T) {
54-
cert := mustTempFile(t)
55-
yaml := `auth_modules:
56-
badtls:
57-
type: userpass
58-
userpass:
59-
username: foo
60-
password: bar
60+
key_file: ` + key + ``,
61+
}, {
62+
"aws-with-tls",
63+
`auth_modules:
64+
awsmod:
65+
type: aws
66+
aws:
67+
region: us-east-1
6168
tls:
69+
insecure_skip_verify: true`,
70+
}, {
71+
"tls-only",
72+
`auth_modules:
73+
pki:
74+
type: tls
75+
tls:
76+
ca_file: ` + ca + `
6277
cert_file: ` + cert + `
63-
`
64-
tmp, _ := os.CreateTemp(t.TempDir(), "cfg-*.yml")
65-
tmp.WriteString(yaml)
66-
tmp.Close()
67-
if _, err := LoadConfig(tmp.Name()); err == nil {
68-
t.Fatalf("expected validation error for missing key_file")
78+
key_file: ` + key + ``,
79+
}}
80+
81+
for _, c := range positive {
82+
tmp, _ := os.CreateTemp(t.TempDir(), "cfg-*.yml")
83+
tmp.WriteString(c.yaml)
84+
tmp.Close()
85+
if _, err := LoadConfig(tmp.Name()); err != nil {
86+
t.Fatalf("%s: expected success, got %v", c.name, err)
87+
}
6988
}
7089
}
7190

72-
func TestLoadConfigValidationErrors(t *testing.T) {
73-
badPath := "/path/does/not/exist"
91+
// ---------------------------- Negative cases ----------------------------
92+
func TestLoadConfigNegativeVariants(t *testing.T) {
93+
cert := mustTempFile(t)
7494
key := mustTempFile(t)
75-
cases := []struct {
95+
96+
negative := []struct {
7697
name string
7798
yaml string
78-
}{
79-
{
80-
"tlsMissingCert",
81-
`auth_modules:
99+
}{{
100+
"userpassMissingPassword",
101+
`auth_modules:
82102
bad:
83103
type: userpass
84-
userpass: {username: u, password: p}
104+
userpass: {username: u}`,
105+
}, {
106+
"tlsMissingCert",
107+
`auth_modules:
108+
bad:
109+
type: tls
85110
tls: {key_file: ` + key + `}`,
86-
},
87-
{
88-
"tlsBadCAPath",
89-
`auth_modules:
111+
}, {
112+
"tlsMissingKey",
113+
`auth_modules:
90114
bad:
91-
type: userpass
92-
userpass: {username: u, password: p}
93-
tls: {ca_file: ` + badPath + `}`,
94-
},
95-
{
96-
"awsNoRegion",
97-
`auth_modules:
115+
type: tls
116+
tls: {cert_file: ` + cert + `}`,
117+
}, {
118+
"tlsMissingConfig",
119+
`auth_modules:
98120
bad:
99-
type: aws
100-
aws: {}`,
101-
},
102-
{
103-
"unsupportedType",
104-
`auth_modules:
121+
type: tls`,
122+
}, {
123+
"tlsWithUserpass",
124+
`auth_modules:
125+
bad:
126+
type: tls
127+
tls: {cert_file: ` + cert + `, key_file: ` + key + `}
128+
userpass: {username: u, password: p}`,
129+
}, {
130+
"tlsWithAPIKey",
131+
`auth_modules:
132+
bad:
133+
type: tls
134+
tls: {cert_file: ` + cert + `, key_file: ` + key + `}
135+
apikey: ZXhhbXBsZQ==`,
136+
}, {
137+
"tlsWithAWS",
138+
`auth_modules:
139+
bad:
140+
type: tls
141+
tls: {cert_file: ` + cert + `, key_file: ` + key + `}
142+
aws: {region: us-east-1}`,
143+
}, {
144+
"tlsIncompleteCert",
145+
`auth_modules:
146+
bad:
147+
type: apikey
148+
apikey: ZXhhbXBsZQ==
149+
tls: {cert_file: ` + cert + `}`,
150+
}, {
151+
"unsupportedType",
152+
`auth_modules:
105153
bad:
106154
type: foobar`,
107-
},
108-
}
109-
for _, c := range cases {
155+
}}
156+
157+
for _, c := range negative {
110158
tmp, _ := os.CreateTemp(t.TempDir(), "cfg-*.yml")
111159
tmp.WriteString(c.yaml)
112160
tmp.Close()
113161
if _, err := LoadConfig(tmp.Name()); err == nil {
114-
t.Fatalf("%s: expected validation error, got nil", c.name)
162+
t.Fatalf("%s: expected validation error, got none", c.name)
115163
}
116164
}
117165
}

examples/auth_modules.yml

Lines changed: 17 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -24,38 +24,32 @@ auth_modules:
2424
password: changeme
2525

2626
###########################################################################
27-
# 3. Userpass + mTLS (per-cluster client certs) #
28-
###########################################################################
29-
prod_tls:
30-
type: userpass
31-
userpass:
32-
username: metrics
33-
password: certsecret
34-
tls:
35-
ca_file: /etc/ssl/prod/ca.pem
36-
cert_file: /etc/ssl/prod/client.pem
37-
key_file: /etc/ssl/prod/client-key.pem
38-
insecure_skip_verify: false
39-
40-
###########################################################################
41-
# 4. API-Key authentication #
27+
# 3. API-Key authentication #
4228
###########################################################################
4329
prod_key:
4430
type: apikey
4531
apikey: BASE64-ENCODED-KEY==
46-
# per-cluster TLS settings can still be supplied if needed
47-
# tls:
48-
# ca_file: /etc/ssl/prod/ca.pem
4932

5033
###########################################################################
51-
# 5. AWS SigV4 signing #
34+
# 5. AWS SigV4 signing with optional TLS settings #
5235
###########################################################################
5336
aws_sigv4:
5437
type: aws
5538
aws:
5639
region: us-east-1
57-
# role_arn is optional; uncomment to assume a role
58-
# role_arn: arn:aws:iam::123456789012:role/metrics-reader
59-
# optional custom TLS
40+
# role_arn is optional
41+
# Optional TLS configuration for transport security
42+
tls:
43+
ca_file: /etc/ssl/ca.pem
44+
insecure_skip_verify: false
45+
46+
###########################################################################
47+
# 6. Client certificate authentication only (no username/password) #
48+
###########################################################################
49+
pki_mtls:
50+
type: tls # This auth type uses ONLY client certificates for authentication
6051
tls:
61-
insecure_skip_verify: true # e.g. self-signed certs on ALB
52+
ca_file: /etc/ssl/pki/ca.pem # Optional: CA for server verification
53+
cert_file: /etc/ssl/pki/client.pem # Required: Client certificate for auth
54+
key_file: /etc/ssl/pki/client-key.pem # Required: Client private key for auth
55+
insecure_skip_verify: false # Optional: Skip server cert validation

0 commit comments

Comments
 (0)