Skip to main content

telelog_server/
auth.rs

1//! Bearer-token authentication and the rules for when the server may run without it.
2
3use std::net::SocketAddr;
4use std::sync::Arc;
5
6use anyhow::{Result, bail};
7use tonic::service::Interceptor;
8use tonic::{Request, Status};
9
10/// Rejects requests whose `authorization` header doesn't carry the expected bearer token.
11#[derive(Clone)]
12pub struct TokenAuth {
13    expected: Option<Arc<str>>,
14}
15
16impl TokenAuth {
17    pub fn new(token: Option<String>) -> Self {
18        TokenAuth {
19            expected: token.map(Into::into),
20        }
21    }
22}
23
24impl Interceptor for TokenAuth {
25    fn call(&mut self, request: Request<()>) -> Result<Request<()>, Status> {
26        let Some(expected) = &self.expected else {
27            return Ok(request);
28        };
29        let presented = request
30            .metadata()
31            .get("authorization")
32            .and_then(|value| value.to_str().ok())
33            .and_then(|value| value.strip_prefix("Bearer "));
34        match presented {
35            Some(token) if constant_time_eq(token.as_bytes(), expected.as_bytes()) => Ok(request),
36            _ => Err(Status::unauthenticated("invalid or missing token")),
37        }
38    }
39}
40
41/// Compares without short-circuiting, so response timing doesn't reveal how much matched.
42fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
43    a.len() == b.len() && a.iter().zip(b).fold(0u8, |acc, (x, y)| acc | (x ^ y)) == 0
44}
45
46/// A random 256-bit token, hex encoded.
47pub fn generate_token() -> Result<String> {
48    let mut bytes = [0u8; 32];
49    getrandom::fill(&mut bytes).map_err(|e| anyhow::anyhow!("reading system randomness: {e}"))?;
50    Ok(bytes.iter().map(|b| format!("{b:02x}")).collect())
51}
52
53/// Refuses configurations that would expose logs to the network without a token, and returns
54/// a warning for ones that are allowed but risky.
55pub fn check_exposure(
56    listen: SocketAddr,
57    has_token: bool,
58    has_tls: bool,
59    allow_unauthenticated: bool,
60) -> Result<Option<&'static str>> {
61    let local = listen.ip().is_loopback();
62    if !local && !has_token && !allow_unauthenticated {
63        bail!(
64            "refusing to listen on {listen} without a token: anyone who can reach it could read your logs. \
65             Set --token (generate one with `telelog-server gen-token`), listen on 127.0.0.1, \
66             or pass --allow-unauthenticated if a proxy in front handles auth."
67        );
68    }
69    Ok(match (local, has_token, has_tls) {
70        (false, false, _) => Some("running without authentication on a non-local address"),
71        (false, true, false) => {
72            Some("token is sent in plain text; enable --tls-cert/--tls-key or terminate TLS in a proxy")
73        }
74        _ => None,
75    })
76}
77
78#[cfg(test)]
79mod tests {
80    use super::*;
81
82    fn request(header: Option<&str>) -> Request<()> {
83        let mut request = Request::new(());
84        if let Some(header) = header {
85            request.metadata_mut().insert("authorization", header.parse().unwrap());
86        }
87        request
88    }
89
90    #[test]
91    fn accepts_only_the_right_token() {
92        let mut auth = TokenAuth::new(Some("s3cret".into()));
93        assert!(auth.call(request(Some("Bearer s3cret"))).is_ok());
94        for bad in [
95            None,
96            Some("Bearer wrong"),
97            Some("Bearer s3cre"),
98            Some("s3cret"),
99            Some("Basic s3cret"),
100        ] {
101            let status = auth.call(request(bad)).unwrap_err();
102            assert_eq!(status.code(), tonic::Code::Unauthenticated, "{bad:?}");
103        }
104    }
105
106    #[test]
107    fn no_token_configured_allows_everything() {
108        assert!(TokenAuth::new(None).call(request(None)).is_ok());
109    }
110
111    #[test]
112    fn generated_tokens_are_long_and_distinct() {
113        let (a, b) = (generate_token().unwrap(), generate_token().unwrap());
114        assert_eq!(a.len(), 64);
115        assert_ne!(a, b);
116    }
117
118    #[test]
119    fn exposure_rules() {
120        let local: SocketAddr = "127.0.0.1:7070".parse().unwrap();
121        let public: SocketAddr = "0.0.0.0:7070".parse().unwrap();
122        assert_eq!(check_exposure(local, false, false, false).unwrap(), None);
123        assert!(check_exposure(public, false, false, false).is_err());
124        assert!(check_exposure(public, false, false, true).unwrap().is_some());
125        assert!(check_exposure(public, true, false, false).unwrap().is_some());
126        assert_eq!(check_exposure(public, true, true, false).unwrap(), None);
127    }
128}