Skip to content

Commit 3c94bf1

Browse files
committed
push: Verify site CA on registration
metrics-cache previously accepted any server certificate (pure Trust On First Use) when registering to the push-agent receiver. This meant the one-time token was vulnerable to MITM who could register as the cluster. The registration/bootstrap now verifies the receiver against a site CA certificate supplied via push.siteCaCertificate before sending the token. This only affects the registration step; the receiver then returns the site CA again which is stored for doing actual pushes. There is an explicit opt-out of verification via push.insecureSkipSiteCAVerification, which defaults to false but allows for the dev environment to spin up without someone having to pass in a certificate (only a token).
1 parent 76e7681 commit 3c94bf1

8 files changed

Lines changed: 116 additions & 5 deletions

File tree

.gitignore

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,7 @@ target
1717

1818
# rejected/unreviewed cargo-insta test snapshots
1919
*.snap.new
20-
*.pending-snap
20+
*.pending-snap
21+
22+
# Custom dev env Helm overrides
23+
devel/custom_values.yaml

charts/cmk-rustik/templates/metrics-cache/deployment.yaml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@ spec:
3838
{{- end }}
3939
{{- if .Values.push.enabled }}
4040
- --push-receiver={{ required "push.url is required when push mode is enabled" .Values.push.url }}
41+
{{- if .Values.push.insecureSkipSiteCaVerification }}
42+
- --push-registration-insecure-skip-site-ca-verification
43+
{{- end }}
4144
{{- end }}
4245
{{- range $k, $v := .Values.emitAll }}
4346
{{- if $v }}
@@ -55,6 +58,14 @@ spec:
5558
name: {{ include "rustik.fullname" . }}-push-registration
5659
key: token
5760
optional: true
61+
{{- if .Values.push.siteCaCertificate }}
62+
- name: CMK_PUSH_AGENT_RECEIVER_SITE_CA_PEM
63+
valueFrom:
64+
secretKeyRef:
65+
name: {{ include "rustik.fullname" . }}-push-registration
66+
key: site-ca-pem
67+
optional: true
68+
{{- end }}
5869
{{- end }}
5970
- name: NODE_NAME
6071
valueFrom:

charts/cmk-rustik/templates/metrics-cache/secret-push-one-time-token.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,7 @@ metadata:
55
name: {{ include "rustik.fullname" . }}-push-registration
66
stringData:
77
token: {{ .Values.push.registrationToken }}
8+
{{- with .Values.push.siteCaCertificate }}
9+
site-ca-pem: {{ . | quote }}
10+
{{- end }}
811
{{- end }}

charts/cmk-rustik/values.yaml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,21 @@ push:
108108
# url is the url of the push-agent receiver for your site. It *includes* the
109109
# site name. Example: https://example.com:8000/production
110110
url: ""
111+
# siteCaCertificate is the PEM-encoded certificate of the Checkmk site CA.
112+
# Used for registering to the Checkmk site's push-agent receiver service.
113+
# It can be found in Setup > Certificate overview, with description
114+
# "Signing the site certificate" and path ending "/ssl/ca.pem". If using helm
115+
# directly from the command line, you can pass this in with:
116+
# --set-file push.siteCaCertificate
117+
siteCaCertificate: ""
118+
# insecureSkipSiteCaVerification *should NOT* ever be set to `true` in
119+
# production. It provides an escape hatch primarily for development of the
120+
# agent itself or for environments that do not care about potential
121+
# man-in-the-middle attacks (which could expose your one-time registration
122+
# token to an attacker who could then register as your cluster). You have
123+
# been warned. When set to true, will trust whatever push-agent receiver
124+
# answers at the configured URL, without veryifying its identity.
125+
insecureSkipSiteCaVerification: false
111126

112127
# hostLabels defines how annotations get imported as labels in Checkmk. By
113128
# default annotations do not become host labels.

devel/values.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,3 +36,7 @@ metricsCache:
3636
push:
3737
enabled: false
3838
url: https://172.18.0.1:8000/heute
39+
# In devel we default this to true so that it's easier to set up against a dev
40+
# site. If you want to test validation, override this in custom_values.yaml
41+
# or set the `just` variable site_ca to a site CA file path.
42+
insecureSkipSiteCaVerification: true

justfile

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ cache_target := "metrics-cache-dev"
77
push := ""
88
push_ott := ""
99
push_url := ""
10+
site_ca := ""
1011
cluster_host_name := ""
1112

1213
# Build an image for Kubernetes using Docker
@@ -28,9 +29,15 @@ kind-load: dockerize kind-create
2829
kind-helm-install:
2930
helm upgrade --install cmk-rustik ./charts/cmk-rustik \
3031
-n checkmk-monitoring --create-namespace -f devel/values.yaml \
32+
{{ if path_exists("devel/custom_values.yaml") == "true" { "-f devel/custom_values.yaml" } else { "" } }} \
3133
{{ if push != "" { "--set push.enabled=" + push } else { "" } }} \
3234
{{ if push_ott != "" { "--set push.registrationToken=" + push_ott } else { "" } }} \
3335
{{ if push_url != "" { "--set push.url=" + push_url } else { "" } }} \
36+
{{ if site_ca != "" { \
37+
"--set push.insecureSkipSiteCaVerification=false " + \
38+
"--set-file push.siteCaCertificate=" + site_ca \
39+
} else { "" } \
40+
}} \
3441
{{ if cluster_host_name != "" { "--set clusterHostName=" + cluster_host_name } else { "" } }}
3542

3643
# Delete the helm deployment from the kind cluster

metrics-cache/src/cli_args.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,20 @@ pub struct CliArgs {
120120
#[arg(long, env = "CMK_PUSH_AGENT_RECEIVER_OTT", hide_env_values = true)]
121121
pub push_registration_ott: Option<String>,
122122

123+
/// CA certificate (PEM) of the Checkmk site, used to verify we are about to
124+
/// register with the correct push-agent receiver.
125+
#[arg(
126+
long,
127+
env = "CMK_PUSH_AGENT_RECEIVER_SITE_CA_PEM",
128+
hide_env_values = true
129+
)]
130+
pub push_registration_pem: Option<String>,
131+
132+
/// Avoid verifying the identity of the configured push-agent receiver
133+
/// during initial registration. Do NOT use in production.
134+
#[arg(long, default_value_t = false)]
135+
pub push_registration_insecure_skip_site_ca_verification: bool,
136+
123137
/// Excluded node role (infix) patterns for cluster-level aggregations,
124138
/// comma-separated
125139
#[arg(long = "excluded-node-role-patterns", value_delimiter = ',')]

metrics-cache/src/push/register.rs

Lines changed: 58 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ use kube::api::Api;
2828
use rcgen::{CertificateParams, DistinguishedName, DnType, KeyPair};
2929
use serde::{Deserialize, Serialize};
3030
use std::collections::BTreeMap;
31-
use tracing::{error, info, trace};
31+
use tracing::{error, info, trace, warn};
3232
use uuid::Uuid;
3333

3434
use crate::cli_args::CliArgs;
@@ -138,10 +138,64 @@ impl<'a> CheckmkPushRegistration<'a> {
138138
)
139139
.into());
140140
};
141-
let client = reqwest::ClientBuilder::new()
142-
.danger_accept_invalid_certs(true) // TOFU for register
143-
.build()?;
144141

142+
// Check whether someone has activated the escape hatch.
143+
let accept_invalid_certs = self
144+
.cli_args
145+
.push_registration_insecure_skip_site_ca_verification;
146+
147+
// Registration must never send the token over an unverified connection
148+
// unless the user explicitly opted out, which they should never do in
149+
// production.
150+
let builder = match (accept_invalid_certs, &self.cli_args.push_registration_pem) {
151+
(false, None) => {
152+
// Case 1: Not accepting invalid certs, but not given a cert
153+
// Intentionally do not reference the unsafe option.
154+
return Err(push::Error::PushMode(
155+
"Push mode was enabled but the agent is not yet registered and \
156+
no Checkmk site CA certificate was provided. The agent needs \
157+
this to know that it is registering to the correct server and \
158+
to prevent man-in-the-middle attacks. If you are trying to \
159+
configure push mode, set the site CA certificate in your helm \
160+
push.siteCaCertificate (on the CLI, --set-file might prove \
161+
useful). The certificate can be downloaded from your Checkmk \
162+
instance under Setup > Certificate overview with description \
163+
\"Signing the site certificate\" and path ending \
164+
\"/ssl/ca.pem\"."
165+
.to_string(),
166+
)
167+
.into());
168+
}
169+
(true, Some(_)) => {
170+
// Case 2: Accepting invalid certs, and also given a cert
171+
return Err(push::Error::PushMode(
172+
"Push mode was enabled with the INSECURE option \
173+
push.insecureSkipSiteCaVerification in your helm values \
174+
but a Checkmk site CA certificate was also supplied with \
175+
push.siteCaCertificate. Exiting because we do not know \
176+
which configuration is intended."
177+
.to_string(),
178+
)
179+
.into());
180+
}
181+
(true, None) => {
182+
// Case 3: Accepting invalid certs
183+
warn!(
184+
"INSECURE option push.insecureSkipSiteCaVerification is enabled, not \
185+
validating push-agent receiver identity while registering. This \
186+
configuration is NOT RECOMMENDED in production."
187+
);
188+
reqwest::ClientBuilder::new().danger_accept_invalid_certs(true)
189+
}
190+
(false, Some(pem)) => {
191+
// Case 4: Pinning the cert
192+
reqwest::ClientBuilder::new()
193+
.tls_certs_only([reqwest::Certificate::from_pem(pem.as_bytes())?])
194+
.danger_accept_invalid_hostnames(true)
195+
}
196+
};
197+
198+
let client = builder.build()?;
145199
let response = client
146200
.post(&url)
147201
.header("Authorization", format!("CMK-TOKEN {}", ott))

0 commit comments

Comments
 (0)