Skip to content

metrics-plugin: add authentication - #12

Open
Nazarevsky wants to merge 3 commits into
masterfrom
feature/metrics-authentication
Open

Nazarevsky wants to merge 3 commits into
masterfrom
feature/metrics-authentication

Conversation

@Nazarevsky

@Nazarevsky Nazarevsky commented Sep 17, 2026 •

Copy link
Copy Markdown
Member

Summary

The /metrics endpoint was previously served without any authentication, exposing node liquidity data to anyone who could reach the port. This adds bearer-token auth, gated behind an explicit opt-out for local/dev use.

What's changed

  • Added --metrics-token-file and --metrics-disable-auth plugin options. The plugin now refuses to start unless one of them is set, so /metrics can't be exposed unauthenticated by accident.
  • Token is read once at startup from the given file (never passed as a CLI arg, so it doesn't leak into the process list, CLN config, or shell history), with surrounding whitespace trimmed.
  • Tokens shorter than 16 characters (or empty) are rejected at startup to catch weak/placeholder values before the endpoint is ever exposed.
  • /metrics is protected via an axum middleware layer checking Authorization: Bearer <token>, using a constant-time comparison to avoid timing side-channels.
  • Updated the README with the new config options, a --metrics-disable-auth warning, curl/Prometheus scrape examples with the bearer token, and guidance to generate the token with openssl rand -hex 32.
  • Added unit tests covering token file parsing (trimming, empty/short/missing file) and the auth middleware (missing/wrong/correct token, unprotected fallback).

Related issue: #11

Adds authorization layer for /metrics endpoint which is a bearer token.
When a query is performed, under the hood the token gets stripped and
compared with the one that is provided to the plugin.

Adds --metrics-token-file (path to a file holding the bearer token) and
--metrics-disable-auth. The plugin refuses to start unless one of the two
is set, so /metrics can't be exposed unauthenticated by accident; the
disable flag logs a warning. Reading the token from a file keeps it out
of the process list, CLN config, and shell history.
Reject tokens shorter than 16 characters (and empty ones) when reading
--metrics-token-file at startup, to catch weak or placeholder values
before /metrics is exposed. Updates the README example to generate a
random token with openssl instead of a short placeholder.
Switches the token plumbing from Arc<str> to String throughout, and
fixes the auth test helper in metrics.rs.
@Nazarevsky Nazarevsky self-assigned this Sep 17, 2026

@olegfomenko olegfomenko left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall looks good. Left some comments, most of them are about small refactoring changes. Also, have you thought about using runes for authorization? like, its kind of a native approach for cln, and from the bearer token perspective, we do not care about the nature of the token we are using

"How often (seconds) to refresh CLN node data for gauge metrics",
);

const OPT_METRICS_TOKEN_FILE: ConfigOption<'static, cln_plugin::options::config_type::String> =

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are already StringConfigOption and DefaultStringConfigOption types declared in cln_plugin::options

"Path to a file containing the bearer token required to access the /metrics endpoint",
);

const OPT_METRICS_DISABLE_AUTH: ConfigOption<

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are already BoolConfigOption and DefaultBoolConfigOption declared in cln_plugin::options


/// Reads the bearer token from `path`, trimming surrounding whitespace so a trailing newline
/// left by `echo` or an editor doesn't become part of the expected token.
async fn read_token_file(path: &str) -> Result<String> {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure about storing token in file... i think devopses may prefer envs cause it may be easier for them to setup secrets securely. With env approach, we can just leave the disable auth option, and if it wasnt set require the existence of env. What fo you think?

let app = Router::new().route("/metrics", get(metrics_handler));
/// Constant-time comparison to avoid leaking the configured token via response-timing side
/// channels.
fn tokens_match(provided: &[u8], expected: &[u8]) -> bool {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do we work with bytes? token is already stored as a string, so we can just compare strings

== 0
}

fn is_authorized(request: &Request, token: &str) -> bool {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also, not sure, but usually i think it may be preferred to fix the token encoding, like URL-compatible Base64. I understand that you may want to leave the decision to the plugin user, but we should be sure that it cant be a potential attack vector

Comment thread metrics-plugin/Cargo.toml
serde_json = { version = "1" }
anyhow = { version = "1" }
tracing = { version = "0.1" }
tower = { version = "0.5", features = ["util"] }

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks like you're using it only in tests, then I'd rather move it into [dev-dependencies]

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants