From 1007fc1b462f6363476c49c4f960a96ed5637702 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20H=C3=A5=C3=A5l?= Date: Wed, 3 May 2023 23:34:12 +0200 Subject: [PATCH] feat(config): Add support for "dynamic" configuration (#216) * feat: Add support for managed switches * feat: Add dynamic configuration * fix: update to go 1.18 * doc(readme): enforce text about protecting the api tokens in Prometheus --- README.md | 68 ++++++++++++++++++++++++++++++++++++++++++++++ pkg/probe/main.go | 11 +++++++- pkg/probe/probe.go | 12 ++++++-- 3 files changed, 88 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 9a1ff19..1a44cdd 100755 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ Prometheus exporter for FortiGate® firewalls. * [Supported Metrics](#supported-metrics) * [Usage](#usage) + + [Dynamic configuration](#dynamic-configuration) + [Available CLI parameters](#available-cli-parameters) + [Fortigate Configuration](#fortigate-configuration) + [Prometheus Configuration](#prometheus-configuration) @@ -324,6 +325,39 @@ Special cases: To probe a FortiGate, do something like `curl 'localhost:9710/probe?target=https://my-fortigate'` +### Dynamic configuration +In use cases where the Fortigates that is to be scraped through the fortigate-exporter is configured in +Prometheus using some discovery method it becomes problematic that the `fortigate-key.yaml` configuration also +has to be updated for each fortigate, and that the fortigate-exporter needs to be restarted on each change. +For that scenario the token can be passed as a query parameter, `token`, to the fortigate. + +Example: +```bash +curl 'localhost:9710/probe?target=https://192.168.2.31&token=ghi6eItWzWewgbrFMsazvBVwDjZzzb' +``` +It is also possible to pass a `profile` query parameter. The value will match an entry in the `fortigate-key.yaml` +file, but only to use the `probes` section for include/exclude directives. + +Example: +```bash +curl 'localhost:9710/probe?target=https://192.168.2.31&token=ghi6eItWzWewgbrFMsazvBVwDjZzzb&profile=fs124e' +``` +The `profile=fs124e` would match the following entry in `fortigate-key.yaml`. + +Example: +```yaml +fs124e: + # token: not used + probes: + include: + - System + - Firewall + exclude: + - System/LinkMonitor +``` + + + ### Available CLI parameters | flag | default value | description | @@ -437,6 +471,40 @@ An example configuration for Prometheus looks something like this: replacement: '[::1]:9710' ``` +If using [Dynamic configuration](#dynamic-configuration): +```yaml + - job_name: 'fortigate_exporter' + metrics_path: /probe + file_sd_configs: + - files: + - /etc/prometheus/file_sd/fws/*.yml + params: + profile: + - fs124e + relabel_configs: + - source_labels: [__address__] + target_label: __param_target + - source_labels: [token] + target_label: __param_token + - source_labels: [__param_target] + regex: '(?:.+)(?::\/\/)([^:]*).*' + target_label: instance + - target_label: __address__ + replacement: '[::1]:9710' + - action: labeldrop + regex: token +``` +> Make sure to use the last labeldrop on the `token` label so that the tokens is not be part of your time series. + +> Since `token` is a label it will be shown in the Prometheus webgui at `http://:9090/targets`. +> +> **Make sure you protect your Prometheus if you add the token part of your prometheus config** +> +> Some options to protect Prometheus: +> - Only expose UI to localhost --web.listen-address="127.0.0.1:9090" +> - Basic authentication access - https://prometheus.io/docs/guides/basic-auth/ +> - **It is your responsibility!** + ### Docker You can either use the automatic builds on diff --git a/pkg/probe/main.go b/pkg/probe/main.go index c58ce96..7d32229 100644 --- a/pkg/probe/main.go +++ b/pkg/probe/main.go @@ -16,7 +16,16 @@ func ProbeHandler(w http.ResponseWriter, r *http.Request) { savedConfig := config.GetConfig() params := r.URL.Query() + paramMap := make(map[string]string) target := params.Get("target") + paramMap["target"] = params.Get("target") + if params.Get("token") != "" { + paramMap["token"] = params.Get("token") + } + if params.Get("profile") != "" { + paramMap["profile"] = params.Get("profile") + } + if target == "" { http.Error(w, "Target parameter missing or empty", http.StatusBadRequest) return @@ -37,7 +46,7 @@ func ProbeHandler(w http.ResponseWriter, r *http.Request) { start := time.Now() pc := &ProbeCollector{} registry.MustRegister(pc) - success, err := pc.Probe(ctx, target, &http.Client{}, savedConfig) + success, err := pc.Probe(ctx, paramMap, &http.Client{}, savedConfig) if err != nil { log.Printf("Probe request rejected; error is: %v", err) http.Error(w, fmt.Sprintf("probe: %v", err), http.StatusBadRequest) diff --git a/pkg/probe/probe.go b/pkg/probe/probe.go index 30c8056..e9f9e60 100644 --- a/pkg/probe/probe.go +++ b/pkg/probe/probe.go @@ -47,8 +47,8 @@ type probeDetailedFunc struct { function probeFunc } -func (p *ProbeCollector) Probe(ctx context.Context, target string, hc *http.Client, savedConfig config.FortiExporterConfig) (bool, error) { - tgt, err := url.Parse(target) +func (p *ProbeCollector) Probe(ctx context.Context, target map[string]string, hc *http.Client, savedConfig config.FortiExporterConfig) (bool, error) { + tgt, err := url.Parse(target["target"]) if err != nil { return false, fmt.Errorf("url.Parse failed: %v", err) } @@ -62,6 +62,14 @@ func (p *ProbeCollector) Probe(ctx context.Context, target string, hc *http.Clie Scheme: tgt.Scheme, Host: tgt.Host, } + + if target["token"] != "" && savedConfig.AuthKeys[config.Target(target["target"])].Token == "" { + // Add the target and its apikey to the savedConfig and use, if exists, a target entry as a template for include/exclude + // This will only happend the "first" time + savedConfig.AuthKeys[config.Target(target["target"])] = config.TargetAuth{Token: config.Token(target["token"]), + Probes: savedConfig.AuthKeys[config.Target(target["profile"])].Probes} + } + c, err := fortiHTTP.NewFortiClient(ctx, u, hc, savedConfig) if err != nil { return false, err