From f7514a8f89e2d35dcd3280ec4a07c3ef9c5d91d0 Mon Sep 17 00:00:00 2001 From: Marc-Antoine Perennou Date: Thu, 16 Apr 2026 18:04:55 +0200 Subject: [PATCH] scrape: add support for fetching credentials through a script To properly support complex authentication scenarii where retrieving a BMC credentials might be non trivial and the cedentials might be temporary, let's support running an external script to gather them. The script will be called like this: {script} bmc-password {target} It should return a JSON like this: {"user": "root", "pass": "toor"} The returned credentials won't be cached and the script will get called every time we scrape a target. --- README.md | 2 ++ cmd/fishymetrics/main.go | 2 ++ common/credentials.go | 4 ++-- http/handlers/scrape.go | 22 ++++++++++++++++++++++ 4 files changed, 28 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index af9501d..7d5e6c0 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,7 @@ Flags: --collector.firmware.modules-exclude="" regex of firmware module to exclude from the scrape --url.extra-params="" extra parameter(s) to parse from the URL. --url.extra-params="param1:alias1,param2:alias2" + --credentials-script="" script to run to gather credentials --credentials.profiles=CREDENTIALS.PROFILES profile(s) with all necessary parameters to obtain BMC credential from secrets backend, i.e. @@ -79,6 +80,7 @@ LOG_PATH= (Default: /var/log/fishymetrics) VAULT_ADDRESS= VAULT_ROLE_ID= VAULT_SECRET_ID= +CREDENTIALS_SCRIPT= HTTP_PROXY= # proxy for http targets HTTPS_PROXY= # proxy for https targets NO_PROXY= # comma-separated list of hosts/CIDRs to bypass proxy diff --git a/cmd/fishymetrics/main.go b/cmd/fishymetrics/main.go index 743857f..52be9b4 100644 --- a/cmd/fishymetrics/main.go +++ b/cmd/fishymetrics/main.go @@ -73,6 +73,7 @@ var ( driveModExclude = a.Flag("collector.drives.modules-exclude", "regex of drive module(s) to exclude from the scrape").Default("").Envar("COLLECTOR_DRIVES_MODULE_EXCLUDE").String() firmwareModExclude = a.Flag("collector.firmware.modules-exclude", "regex of firmware module(s) to exclude from the scrape").Default("").Envar("COLLECTOR_FIRMWARE_MODULE_EXCLUDE").String() urlExtraParams = a.Flag("url.extra-params", `extra parameter(s) to parse from the URL. --url.extra-params="param1:alias1,param2:alias2"`).Default("").Envar("URL_EXTRA_PARAMS").String() + credentialsScript = a.Flag("credentials-script", "script to run to get the BMC credentials").Default("").Envar("BMC_CREDENTIALS_SCRIPT").String() _ = common.CredentialProf(a.Flag("credentials.profiles", `profile(s) with all necessary parameters to obtain BMC credential from secrets backend, i.e. --credentials.profiles=" @@ -242,6 +243,7 @@ func main() { // Create scrape handler configuration scrapeConfig := &handlers.ScrapeConfig{ Vault: vault, + CredentialsScript: *credentialsScript, Excludes: excludes, URLExtraParamsMap: urlExtraParamsMap, ExtraParamsAliases: extraParamsAliases, diff --git a/common/credentials.go b/common/credentials.go index 6a9c142..60a47b3 100644 --- a/common/credentials.go +++ b/common/credentials.go @@ -47,8 +47,8 @@ type ChassisCredentials struct { } type Credential struct { - User string - Pass string + User string `json:"user"` + Pass string `json:"pass"` } type ProfileFlag struct { diff --git a/http/handlers/scrape.go b/http/handlers/scrape.go index dc6104b..6d1b719 100644 --- a/http/handlers/scrape.go +++ b/http/handlers/scrape.go @@ -18,9 +18,11 @@ package handlers import ( "context" + "encoding/json" "fmt" "net/http" "net/url" + "os/exec" "strings" "github.com/comcast/fishymetrics/common" @@ -38,6 +40,7 @@ import ( // ScrapeConfig holds configuration for scrape handlers type ScrapeConfig struct { Vault *fishy_vault.Vault + CredentialsScript string Excludes map[string]interface{} URLExtraParamsMap map[string]string ExtraParamsAliases map[string]string @@ -114,6 +117,25 @@ func handler(ctx context.Context, w http.ResponseWriter, r *http.Request, cfg *S // Set configurations in common package for use in credential retrieval common.ExtraParamsAliases = extraParamsAliases + // check if credentials script is configured + if cfg.CredentialsScript != "" { + // Don't check if we already have credentials for this target. The script feature is there for custom scenarii where the credentials might be temporary. + // Running e.g. "/usr/bin/my-script bmc-password 10.2.1.42" should return a json like {"user":"root", "pass":"toor"} + out, err := exec.Command(cfg.CredentialsScript, "bmc-password", target).Output() + if err != nil { + log.Error("issue retrieving credentials from script using target "+target, zap.Error(err), zap.Any("trace_id", ctx.Value(logging.TraceIDKey("traceID")))) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + credential := &common.Credential{} + if err = json.Unmarshal(out, credential); err != nil { + log.Error("issue parsing credentials retrieved from script using target "+target, zap.Error(err), zap.Any("trace_id", ctx.Value(logging.TraceIDKey("traceID")))) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + common.ChassisCreds.Set(target, credential) + } + // check if vault is configured if cfg.Vault != nil { // check if ChassisCredentials hashmap contains the credentials we need otherwise get them from vault