pub mod directory;
pub mod idp_ca;
pub mod oidc;
pub mod plane;
pub mod saml;
pub mod scim;
use std::collections::HashMap;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex, RwLock};
pub const FLOW_TTL_MS: i64 = 10 * 60_000;
pub const SECOND_TTL_MS: i64 = 5 * 60_000;
pub const MAX_FLOWS: usize = 10_000;
pub const MAX_FLOWS_PER_ACCOUNT: usize = 500;
pub const SKEW_SECS: i64 = 120;
pub const IDP_CACHE_MS: i64 = 60 * 60_000;
pub const MAX_IDP_BODY: usize = 1 << 20;
pub const BEGINS_PER_MINUTE: u32 = 120;
pub const SCIM_PER_MINUTE: u32 = 600;
pub const FAILURES_PER_MINUTE: u32 = 60;
pub trait Egress: Send + Sync {
fn get(&self, url: &str) -> Result<(u16, String), String>;
fn post_form(
&self,
url: &str,
authorization: &str,
body: &str,
) -> Result<(u16, String), String>;
fn get_trusting(&self, url: &str, ca: &[Vec<u8>]) -> Result<(u16, String), String> {
if ca.is_empty() {
return self.get(url);
}
Err("this server's egress cannot trust an identity provider's own CA".to_owned())
}
fn post_form_trusting(
&self,
url: &str,
authorization: &str,
body: &str,
ca: &[Vec<u8>],
) -> Result<(u16, String), String> {
if ca.is_empty() {
return self.post_form(url, authorization, body);
}
Err("this server's egress cannot trust an identity provider's own CA".to_owned())
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct PrivateNets(Vec<(std::net::IpAddr, u8)>);
impl PrivateNets {
pub fn parse(text: &str) -> Result<PrivateNets, String> {
use std::net::IpAddr;
let mut nets = Vec::new();
for word in text.split(|c: char| c == ',' || c.is_whitespace()).filter(|w| !w.is_empty()) {
let (addr, len) = word
.split_once('/')
.ok_or_else(|| format!("{word:?} is not a network: name it as address/length (10.0.2.100/32)"))?;
let ip: IpAddr = addr.parse().map_err(|_| format!("{word:?}: {addr:?} is not an address"))?;
let len: u8 = len.parse().map_err(|_| format!("{word:?}: {len:?} is not a prefix length"))?;
let private = match ip {
IpAddr::V4(v4) => {
len <= 32 && PRIVATE_V4.iter().any(|(net, plen)| len >= *plen && v4_in(v4, *net, *plen))
}
IpAddr::V6(v6) => {
v6.to_ipv4_mapped().is_none()
&& (7..=128).contains(&len)
&& v6.segments()[0] & 0xfe00 == 0xfc00
}
};
if !private {
return Err(format!(
"{word:?} is not inside a private network: an operator may list RFC 1918 (10/8, 172.16/12, \
192.168/16), carrier-grade NAT (100.64/10) or unique-local IPv6 (fc00::/7) ranges only — never \
loopback, link-local (the cloud's metadata service), documentation or public ones"
));
}
nets.push((ip, len));
}
Ok(PrivateNets(nets))
}
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
pub fn contains(&self, ip: std::net::IpAddr) -> bool {
use std::net::IpAddr;
let ip = match ip {
IpAddr::V6(v6) => v6.to_ipv4_mapped().map_or(ip, IpAddr::V4),
v4 => v4,
};
self.0.iter().any(|(net, len)| match (ip, net) {
(IpAddr::V4(a), IpAddr::V4(n)) => v4_in(a, *n, *len),
(IpAddr::V6(a), IpAddr::V6(n)) => {
let mask = if *len == 0 { 0 } else { u128::MAX << (128 - u32::from(*len)) };
u128::from(a) & mask == u128::from(*n) & mask
}
_ => false,
})
}
}
const PRIVATE_V4: [(std::net::Ipv4Addr, u8); 4] = [
(std::net::Ipv4Addr::new(10, 0, 0, 0), 8),
(std::net::Ipv4Addr::new(172, 16, 0, 0), 12),
(std::net::Ipv4Addr::new(192, 168, 0, 0), 16),
(std::net::Ipv4Addr::new(100, 64, 0, 0), 10),
];
fn v4_in(ip: std::net::Ipv4Addr, net: std::net::Ipv4Addr, len: u8) -> bool {
let mask = if len == 0 { 0 } else { u32::MAX << (32 - u32::from(len)) };
u32::from(ip) & mask == u32::from(net) & mask
}
impl std::fmt::Display for PrivateNets {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let words: Vec<String> = self.0.iter().map(|(ip, len)| format!("{ip}/{len}")).collect();
f.write_str(&words.join(","))
}
}
pub fn url_allowed(url: &str, allow_loopback_http: bool) -> Result<(), String> {
url_allowed_in(url, allow_loopback_http, &PrivateNets::default())
}
fn loopback_http(url: &str) -> bool {
let lower = url.trim().to_ascii_lowercase();
let Some(rest) = lower.strip_prefix("http://") else { return false };
let host = rest.split(['/', '?', '#']).next().unwrap_or_default();
let host = match host.rsplit_once(':') {
Some((h, port)) if port.bytes().all(|b| b.is_ascii_digit()) => h,
_ => host,
};
matches!(host, "127.0.0.1" | "localhost" | "[::1]")
}
pub fn url_allowed_in(url: &str, allow_loopback_http: bool, nets: &PrivateNets) -> Result<(), String> {
if allow_loopback_http
&& loopback_http(url)
&& url.to_ascii_lowercase().starts_with("http://")
{
return Ok(());
}
let lower = url.trim().to_ascii_lowercase();
let Some(rest) = lower.strip_prefix("https://") else {
return Err(format!("{url:?} is not an https URL"));
};
let authority = rest.split(['/', '?', '#']).next().unwrap_or_default();
if authority.contains('@') {
return Err(format!("{url:?} carries credentials"));
}
let host = if let Some(v6) = authority.strip_prefix('[') {
v6.split(']').next().unwrap_or_default().to_owned()
} else {
authority
.rsplit_once(':')
.map_or(authority, |(h, _)| h)
.to_owned()
};
if host.is_empty() {
return Err(format!("{url:?} names no host"));
}
if host == "localhost"
|| host.ends_with(".localhost")
|| host.ends_with(".internal")
|| host.ends_with(".local")
{
return Err(format!("{url:?} is a local name"));
}
if let Ok(ip) = host.parse::<std::net::IpAddr>() {
if let Some(why) = address_forbidden_in(ip, false, nets) {
return Err(format!("{url:?} is a {why} address"));
}
}
Ok(())
}
pub fn address_forbidden(ip: std::net::IpAddr, allow_loopback: bool) -> Option<&'static str> {
use std::net::{IpAddr, Ipv4Addr};
let v6 = match ip {
IpAddr::V4(v4) => return v4_forbidden(v4, allow_loopback),
IpAddr::V6(v6) => v6,
};
if v6.is_loopback() {
return (!allow_loopback).then_some("loopback");
}
if v6.is_unspecified() {
return Some("unspecified");
}
if let Some(v4) = v6.to_ipv4_mapped() {
return v4_forbidden(v4, allow_loopback);
}
let s = v6.segments();
let embedded = Ipv4Addr::from((u32::from(s[6]) << 16) | u32::from(s[7]));
if s[..6] == [0; 6] || (s[0] == 0x64 && s[1] == 0xff9b && s[2..6] == [0; 4]) {
return v4_forbidden(embedded, allow_loopback);
}
match s[0] {
x if x & 0xfe00 == 0xfc00 => Some("unique-local (private)"),
x if x & 0xffc0 == 0xfe80 => Some("link-local"),
x if x & 0xffc0 == 0xfec0 => Some("site-local"),
x if x & 0xff00 == 0xff00 => Some("multicast"),
0x2001 if s[1] == 0x0db8 => Some("documentation"),
_ => None,
}
}
fn v4_forbidden(v4: std::net::Ipv4Addr, allow_loopback: bool) -> Option<&'static str> {
let o = v4.octets();
if v4.is_loopback() {
return (!allow_loopback).then_some("loopback");
}
if v4.is_private() {
return Some("private");
}
if v4.is_link_local() {
return Some("link-local — the cloud's metadata service lives here");
}
if v4.is_unspecified() || o[0] == 0 {
return Some("unspecified");
}
if v4.is_broadcast() {
return Some("broadcast");
}
if v4.is_multicast() {
return Some("multicast");
}
if o[0] == 100 && (64..=127).contains(&o[1]) {
return Some("carrier-grade NAT (shared, private)");
}
if v4.is_documentation() {
return Some("documentation");
}
if o[0] == 198 && (o[1] == 18 || o[1] == 19) {
return Some("benchmarking");
}
if o[0] == 192 && o[1] == 0 && o[2] == 0 {
return Some("IETF protocol assignments");
}
if o[0] >= 240 {
return Some("reserved");
}
None
}
pub fn addresses_allowed(host: &str, addrs: &[std::net::SocketAddr], allow_loopback: bool) -> Result<(), String> {
addresses_allowed_in(host, addrs, allow_loopback, &PrivateNets::default())
}
pub fn address_forbidden_in(ip: std::net::IpAddr, allow_loopback: bool, nets: &PrivateNets) -> Option<&'static str> {
if nets.contains(ip) {
return None;
}
address_forbidden(ip, allow_loopback)
}
pub fn addresses_allowed_in(host: &str, addrs: &[std::net::SocketAddr], allow_loopback: bool, nets: &PrivateNets) -> Result<(), String> {
if addrs.is_empty() {
return Err(format!("{host} resolves to no address"));
}
for addr in addrs {
if let Some(why) = address_forbidden_in(addr.ip(), allow_loopback, nets) {
return Err(format!("{host} resolves to {} ({why}): an identity provider is never on this box's own network", addr.ip()));
}
}
Ok(())
}
#[derive(Clone)]
pub struct Flow {
pub account: String,
pub saml: bool,
pub console_origin: String,
pub expires_ms: i64,
pub nonce: String,
pub verifier: String,
pub redirect_uri: String,
pub request_id: String,
}
#[derive(Clone)]
pub struct Second {
pub account: String,
pub principal_id: String,
pub register: bool,
pub session_openssh: String,
pub session_expires_ms: i64,
pub expires_ms: i64,
}
#[derive(Clone)]
pub struct CachedIdp {
#[cfg(feature = "sso")]
pub discovery: nornir_auth_oidc::rp::Discovery,
pub jwks: String,
pub fetched_ms: i64,
}
#[derive(Default)]
struct Windows {
counts: HashMap<String, (i64, u32)>,
}
#[derive(Default)]
pub struct SsoState {
egress: RwLock<Option<Arc<dyn Egress>>>,
allow_loopback_http: AtomicBool,
private_nets: Arc<RwLock<PrivateNets>>,
flows: Mutex<HashMap<String, Flow>>,
seconds: Mutex<HashMap<String, Second>>,
saml_seen: Mutex<HashMap<String, i64>>,
idps: Mutex<HashMap<String, CachedIdp>>,
windows: Mutex<Windows>,
}
fn lock<T>(m: &Mutex<T>) -> std::sync::MutexGuard<'_, T> {
match m.lock() {
Ok(g) => g,
Err(poisoned) => poisoned.into_inner(),
}
}
impl SsoState {
pub fn set_egress(&self, egress: Arc<dyn Egress>) {
*self.egress.write().unwrap_or_else(|p| p.into_inner()) = Some(egress);
}
pub fn egress(&self) -> Option<Arc<dyn Egress>> {
self.egress
.read()
.unwrap_or_else(|p| p.into_inner())
.clone()
}
pub fn set_allow_loopback_http(&self, allow: bool) {
self.allow_loopback_http.store(allow, Ordering::SeqCst);
}
pub fn allow_loopback_http(&self) -> bool {
self.allow_loopback_http.load(Ordering::SeqCst)
}
pub fn set_private_nets(&self, nets: PrivateNets) {
*self.private_nets.write().unwrap_or_else(|p| p.into_inner()) = nets;
}
pub fn private_nets(&self) -> PrivateNets {
self.private_nets.read().unwrap_or_else(|p| p.into_inner()).clone()
}
pub fn private_nets_handle(&self) -> Arc<RwLock<PrivateNets>> {
Arc::clone(&self.private_nets)
}
pub fn begin(&self, state_hash: String, flow: Flow, now: i64) -> Result<(), String> {
let mut flows = lock(&self.flows);
flows.retain(|_, f| f.expires_ms > now);
if flows.len() >= MAX_FLOWS {
return Err(
"too many sign-ins are waiting for their identity providers; try again in a minute"
.to_owned(),
);
}
if flows.values().filter(|f| f.account == flow.account).count() >= MAX_FLOWS_PER_ACCOUNT {
return Err(format!(
"too many sign-ins to {} are waiting; try again in a minute",
flow.account
));
}
flows.insert(state_hash, flow);
Ok(())
}
pub fn take_flow(&self, state_hash: &str, now: i64) -> Option<Flow> {
let mut flows = lock(&self.flows);
flows.remove(state_hash).filter(|f| f.expires_ms > now)
}
pub fn park_second(&self, ticket_hash: String, second: Second, now: i64) -> Result<(), String> {
let mut seconds = lock(&self.seconds);
seconds.retain(|_, s| s.expires_ms > now);
if seconds.len() >= MAX_FLOWS {
return Err("too many sign-ins are waiting for their passkeys".to_owned());
}
seconds.insert(ticket_hash, second);
Ok(())
}
pub fn take_second(&self, ticket_hash: &str, now: i64) -> Option<Second> {
lock(&self.seconds)
.remove(ticket_hash)
.filter(|s| s.expires_ms > now)
}
pub fn first_sight(&self, assertion_id: &str, until_ms: i64, now: i64) -> bool {
let mut seen = lock(&self.saml_seen);
seen.retain(|_, until| *until > now);
if seen.contains_key(assertion_id) {
return false;
}
if seen.len() >= 100_000 {
return false;
}
seen.insert(assertion_id.to_owned(), until_ms.max(now + FLOW_TTL_MS));
true
}
pub fn cached_idp(&self, issuer: &str, now: i64) -> Option<CachedIdp> {
lock(&self.idps)
.get(issuer)
.filter(|c| now - c.fetched_ms < IDP_CACHE_MS)
.cloned()
}
pub fn cache_idp(&self, issuer: &str, idp: CachedIdp) {
let mut idps = lock(&self.idps);
if idps.len() >= 4096 {
idps.clear();
}
idps.insert(issuer.to_owned(), idp);
}
pub fn allow(&self, key: &str, limit: u32, now: i64) -> bool {
let minute = now / 60_000;
let mut w = lock(&self.windows);
if w.counts.len() > 50_000 {
w.counts.retain(|_, (m, _)| *m == minute);
}
let entry = w.counts.entry(key.to_owned()).or_insert((minute, 0));
if entry.0 != minute {
*entry = (minute, 0);
}
entry.1 += 1;
entry.1 <= limit
}
pub fn counted(&self, key: &str, now: i64) -> u32 {
let minute = now / 60_000;
lock(&self.windows)
.counts
.get(key)
.filter(|(m, _)| *m == minute)
.map_or(0, |(_, n)| *n)
}
}
pub fn rfc3339(unix: i64) -> String {
let days = unix.div_euclid(86_400);
let secs = unix.rem_euclid(86_400);
let z = days + 719_468;
let era = z.div_euclid(146_097);
let doe = z - era * 146_097;
let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365;
let y = yoe + era * 400;
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
let mp = (5 * doy + 2) / 153;
let d = doy - (153 * mp + 2) / 5 + 1;
let m = if mp < 10 { mp + 3 } else { mp - 9 };
let y = if m <= 2 { y + 1 } else { y };
format!(
"{y:04}-{m:02}-{d:02}T{:02}:{:02}:{:02}Z",
secs / 3600,
secs % 3600 / 60,
secs % 60
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn urls_an_admin_types_cannot_reach_inside() {
assert!(url_allowed("https://login.example.com/x", false).is_ok());
for bad in [
"http://login.example.com/",
"https://127.0.0.1/",
"https://10.0.0.5:8443/",
"https://169.254.169.254/latest/meta-data",
"https://[::1]/",
"https://[fd00::1]/",
"https://localhost/",
"https://user:pw@login.example.com/",
"https://100.64.1.1/",
"ftp://login.example.com/",
] {
assert!(url_allowed(bad, false).is_err(), "{bad} was allowed");
}
assert!(url_allowed("http://127.0.0.1:9000/x", true).is_ok());
assert!(url_allowed("http://10.0.0.5/x", true).is_err());
assert!(
url_allowed("https://127.0.0.1/", true).is_err(),
"the test door is http loopback only"
);
}
#[test]
fn addresses_inside_the_box_are_refused() {
use std::net::IpAddr;
for bad in [
"127.0.0.1", "127.8.9.10", "10.1.2.3", "172.16.0.1", "192.168.1.1", "169.254.169.254", "100.64.0.1",
"0.0.0.0", "255.255.255.255", "224.0.0.1", "192.0.2.10", "198.18.0.1", "240.0.0.1",
"::1", "::", "fd00::1", "fe80::1", "ff02::1", "2001:db8::1", "::ffff:127.0.0.1", "::ffff:10.0.0.1",
"::ffff:169.254.169.254", "64:ff9b::a00:1", "fec0::1",
] {
let ip: IpAddr = bad.parse().unwrap();
assert!(address_forbidden(ip, false).is_some(), "{bad} was allowed");
}
for good in ["93.184.216.34", "1.1.1.1", "2606:4700:4700::1111", "2a00:1450:4001::200e"] {
let ip: IpAddr = good.parse().unwrap();
assert_eq!(address_forbidden(ip, false), None, "{good} was refused");
}
assert_eq!(address_forbidden("127.0.0.1".parse().unwrap(), true), None);
assert_eq!(address_forbidden("::1".parse().unwrap(), true), None);
assert!(address_forbidden("10.0.0.1".parse().unwrap(), true).is_some());
assert!(address_forbidden("169.254.169.254".parse().unwrap(), true).is_some());
}
#[test]
fn every_resolved_address_must_pass() {
let a = |s: &str| -> std::net::SocketAddr { s.parse().unwrap() };
assert!(addresses_allowed("idp.example.test", &[a("93.184.216.34:443")], false).is_ok());
let mixed = addresses_allowed("idp.example.test", &[a("93.184.216.34:443"), a("10.0.0.7:443")], false);
assert!(mixed.unwrap_err().contains("10.0.0.7"), "one private answer among public ones passed");
assert!(addresses_allowed("idp.example.test", &[], false).is_err());
}
#[test]
fn only_private_networks_may_be_listed() {
let nets = PrivateNets::parse("10.0.2.100/32, 172.16.0.0/12 192.168.7.0/24,100.64.0.0/10\nfd00::/8").expect("private ranges");
assert_eq!(nets.to_string(), "10.0.2.100/32,172.16.0.0/12,192.168.7.0/24,100.64.0.0/10,fd00::/8");
assert_eq!(PrivateNets::parse("").expect("empty"), PrivateNets::default());
assert_eq!(PrivateNets::parse(" ").expect("blank"), PrivateNets::default());
for bad in [
"127.0.0.0/8", "127.0.0.1/32", "169.254.169.254/32", "169.254.0.0/16", "0.0.0.0/0", "8.8.8.0/24",
"10.0.0.0/7", "192.0.2.0/24", "224.0.0.0/4", "::1/128", "fe80::/10", "::/0", "2001:db8::/32",
"10.0.0.1", "10.0.0.0/33", "fd00::/129", "not-a-net", "::ffff:10.0.0.0/104",
] {
assert!(PrivateNets::parse(bad).is_err(), "{bad} was accepted");
}
}
#[test]
fn a_listed_network_admits_its_addresses_and_no_other() {
use std::net::IpAddr;
let nets = PrivateNets::parse("10.0.2.100/32,fd00:aa::/32").expect("nets");
let ip = |s: &str| -> IpAddr { s.parse().unwrap() };
assert!(nets.contains(ip("10.0.2.100")));
assert!(nets.contains(ip("::ffff:10.0.2.100")), "v4-in-v6 is judged as v4");
assert!(nets.contains(ip("fd00:aa::7")));
assert!(!nets.contains(ip("10.0.2.101")));
assert_eq!(address_forbidden_in(ip("10.0.2.100"), false, &nets), None);
assert!(address_forbidden_in(ip("10.0.2.101"), false, &nets).is_some());
assert!(address_forbidden_in(ip("169.254.169.254"), false, &nets).is_some());
assert!(address_forbidden_in(ip("127.0.0.1"), false, &nets).is_some());
assert_eq!(address_forbidden_in(ip("93.184.216.34"), false, &nets), None, "public stays public");
assert!(url_allowed_in("https://10.0.2.100:8443/realms/cov", false, &nets).is_ok());
assert!(url_allowed_in("https://10.0.2.101:8443/realms/cov", false, &nets).is_err());
assert!(url_allowed_in("http://10.0.2.100:8443/realms/cov", false, &nets).is_err(), "still https only");
assert!(url_allowed("https://10.0.2.100:8443/realms/cov", false).is_err(), "no list: today's guard");
let a = |s: &str| -> std::net::SocketAddr { s.parse().unwrap() };
assert!(addresses_allowed_in("idp.corp.internal.example", &[a("10.0.2.100:8443")], false, &nets).is_ok());
assert!(addresses_allowed_in("idp.corp.internal.example", &[a("10.0.2.100:8443"), a("10.9.9.9:8443")], false, &nets).is_err());
let s = SsoState::default();
assert!(s.private_nets().is_empty());
s.set_private_nets(nets.clone());
assert_eq!(*s.private_nets_handle().read().unwrap(), nets, "the resolver's handle sees the list");
}
#[test]
fn flows_are_single_use_and_expire() {
let s = SsoState::default();
let flow = |account: &str| Flow {
account: account.into(),
saml: false,
console_origin: String::new(),
expires_ms: 1_000 + FLOW_TTL_MS,
nonce: String::new(),
verifier: String::new(),
redirect_uri: String::new(),
request_id: String::new(),
};
s.begin("a".into(), flow("acme"), 1_000).unwrap();
assert!(s.take_flow("a", 2_000).is_some());
assert!(s.take_flow("a", 2_000).is_none(), "a flow was taken twice");
s.begin("b".into(), flow("acme"), 1_000).unwrap();
assert!(
s.take_flow("b", 1_000 + FLOW_TTL_MS + 1).is_none(),
"an expired flow was taken"
);
for i in 0..MAX_FLOWS_PER_ACCOUNT {
s.begin(format!("x{i}"), flow("acme"), 1_000).unwrap();
}
assert!(s.begin("over".into(), flow("acme"), 1_000).is_err());
assert!(s.begin("other".into(), flow("globex"), 1_000).is_ok());
}
#[test]
fn an_assertion_is_consumed_once() {
let s = SsoState::default();
assert!(s.first_sight("_a1", 10_000_000, 1_000));
assert!(
!s.first_sight("_a1", 10_000_000, 2_000),
"a replayed assertion was accepted"
);
assert!(s.first_sight("_a2", 10_000_000, 2_000));
}
#[test]
fn rate_windows_count_per_minute() {
let s = SsoState::default();
for _ in 0..3 {
assert!(s.allow("k", 3, 60_000));
}
assert!(!s.allow("k", 3, 61_000));
assert!(s.allow("k", 3, 120_000), "a new minute starts afresh");
}
#[test]
fn rfc3339_is_utc() {
assert_eq!(rfc3339(0), "1970-01-01T00:00:00Z");
assert_eq!(rfc3339(1_800_000_000), "2027-01-15T08:00:00Z");
assert_eq!(rfc3339(951_782_400), "2000-02-29T00:00:00Z");
}
}