Skip to content

metrics-plugin: add authentication - #12

Merged
Nazarevsky merged 7 commits into
masterfrom
feature/metrics-authentication
Nov 5, 2026
Merged

Nazarevsky merged 7 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

Comment thread metrics-plugin/src/main.rs Resolved
Comment thread metrics-plugin/src/main.rs Resolved
Comment thread metrics-plugin/src/main.rs Resolved
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

@Nazarevsky Nazarevsky Oct 30, 2026 •

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Ohhh, than question opens a pandora box that I'm really happy to share 😈

The thing is related to so-called remote timing attacks against string comparisons.

Let's consider a case where we compare strings rather than bytes. Rust comrpares string values character by character. This allows an attacker to brute-force a password based on response times. The guard checks the first character, then the second, then the third, and so on. As soon as it encounters an incorrect character, it immediately rejects the input and stops checking.

For example, a guess that matches the first five characters takes slightly longer to reject than one that only matches the first character, simply because the guard has to check more characters before giving up. This creates an opportunity to gradually figure out the correct sequence of characters.

In our case, the comparison function always takes a constant-time approach. It uses a combination of XOR and OR operations to check the result. Even if some characters are incorrect, it processes the entire provided string, making it much harder to determine whether a particular character is invalid based on execution time.

And yes, I know the probability of successfully exploiting this is relatively low, and we also have to account for external noise (e.g. network jitter). However, by performing thousands or even millions of requests and averaging the response times, the actual timing difference (caused by comparing one additional byte) can still emerge statistically.

== 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

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

The encoding choice itself isn't really an attack vector in my opninion - the comparison is already byte-equal and constant-time regardless of what characters are in the token. The HTTP layer (the axum header parser) already rejects control characters like newlines in header values before our code even sees them, so header injection via token should not be possible here

Comment thread metrics-plugin/Cargo.toml Resolved
@Nazarevsky

Copy link
Copy Markdown
Member Author

Thank you for this review @olegfomenko! You got a lot of really good suggestions over here 🔥

Potentially we can provide rune authentication to /metrics. Originally, my intention was to make it compatible with Prometheus. I'm not sure that it does allow to add some additional authentication methods, so I'd rather fall back with something that Prometheus supports.

Also, applied your suggestions regarding options and token provision (now we use env var).

Besides, regarding string comparison for tokens. Normal string comparison stops at the first wrong byte, so response times leak how much of a guess is correct. Our guard instead uses a constant-time XOR/OR comparison over the whole input. The leak is tiny, but averaging many requests can expose it, so this defense matters.

So, if you think that we still need to change something - I'm as always open for discussions 🙌

Matches the style already used in rpc-log-plugin.
Env vars are easier for devops to inject securely via existing secret
tooling (k8s secretKeyRef, systemd EnvironmentFile, Docker secrets)
than requiring a file on disk.
It's only used by ServiceExt in tests, so it shouldn't ship in the
release binary.
@Nazarevsky
Nazarevsky force-pushed the feature/metrics-authentication branch from 0c0c829 to b5172f1 Compare October 30, 2026 18:18
@olegfomenko

Copy link
Copy Markdown
Member

LGTM

@Nazarevsky
Nazarevsky merged commit 0238cee into master Nov 5, 2026
1 check passed
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