Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,10 @@ This is best achieved by running rootless containers with a dedicated user for k
- [Docker](https://docs.docker.com/engine/security/rootless/)
- [podman](https://github.com/containers/podman/blob/main/docs/tutorials/rootless_tutorial.md)

Where a seccomp profile is not an option, `use_keyring: false` falls back to keeping the keys in the process memory of
the app instead of the kernel keyring. That is weaker: the pages are not locked, so a key can reach swap or a core dump,
and it is logged as a warning on startup. Prefer the seccomp profile above where you can.

Example docker:

docker run \
Expand Down
9 changes: 9 additions & 0 deletions config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -110,5 +110,14 @@ cookie_samesite: 'strict'
# reverse proxy that sets these headers, they are spoofable otherwise.
trust_proxy_headers: false

# Whether to use the Linux kernel keyring for session key storage.
# Ignored on non-Linux platforms (always falls back to in-memory).
# Set to false if your container runtime blocks the keyctl/add_key/request_key
# syscalls — for example when running the Docker image on macOS with Docker
# Desktop and no custom seccomp profile. The in-memory store is slightly
# weaker (keys live in process memory rather than the kernel) but fully
# functional for single-instance deployments.
use_keyring: true

listen: '::'
port: 8080
6 changes: 6 additions & 0 deletions src/config/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,11 @@ pub struct Config {
// only enable behind a reverse proxy: forwarding headers are
// client-controlled and would let rate limiting be evaded
pub trust_proxy_headers: bool,
// use the linux kernel keyring for session key storage (linux only).
// set to false if your container runtime blocks keyctl/add_key/request_key
// (e.g. docker desktop on macos without a custom seccomp profile).
// ignored on non-linux platforms, which always use the in-memory store.
pub use_keyring: bool,
pub search: Search,
#[serde(alias = "LDAP", alias = "Ldap")]
pub ldap: Ldap,
Expand Down Expand Up @@ -67,6 +72,7 @@ impl Default for Config {
session_lifetime: Duration::from_secs(60 * 60),
cookie_samesite: cookie::SameSite::Strict,
trust_proxy_headers: false,
use_keyring: true,
search: Default::default(),
ldap: Default::default(),
oidc: Default::default(),
Expand Down
5 changes: 3 additions & 2 deletions src/keepass/keepass.rs
Original file line number Diff line number Diff line change
Expand Up @@ -373,8 +373,9 @@ mod tests {

let (mut key, enc) = keepass.to_enc().unwrap();

key.store(config.db_session_timeout).unwrap();
let ret_key = SecretKey::retrieve(&key.key_id, config.db_session_timeout).unwrap();
// tests always use the in-memory store; the keyring is unavailable in most CI/test environments
key.store(config.db_session_timeout, false).unwrap();
let ret_key = SecretKey::retrieve(&key.key_id, config.db_session_timeout, false).unwrap();

let dec = KeePass::from_enc(&config, ret_key, enc).unwrap();

Expand Down
72 changes: 57 additions & 15 deletions src/keepass/key.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,9 @@ use crate::auth::gen_token;

#[cfg(target_os = "linux")]
mod keyring;
#[cfg(target_os = "linux")]
use keyring as store;

#[cfg(any(not(target_os = "linux"), test))]
mod memory;
#[cfg(not(target_os = "linux"))]
use memory as store;
// always compiled: used on non-linux, in tests, and when use_keyring=false on linux
pub(super) mod memory;

pub type KeyId = String;

Expand All @@ -38,6 +34,7 @@ pub struct SecretKey {
pub key_id: KeyId,
data: SecretBox<[u8]>,
timeout: Duration,
use_keyring: bool,
}

impl SecretKey {
Expand All @@ -46,34 +43,79 @@ impl SecretKey {
key_id: gen_token(ID_LENGTH),
data: SecretBox::new(secret),
timeout: Duration::default(),
use_keyring: false,
}
}

pub fn retrieve(key_id: &KeyId, timeout: Duration) -> Result<Self> {
let data = store::retrieve(key_id, timeout)?;
pub fn retrieve(key_id: &KeyId, timeout: Duration, use_keyring: bool) -> Result<Self> {
let data = Self::dispatch_retrieve(key_id, timeout, use_keyring)?;

Ok(
Self {
key_id: key_id.clone(),
data: SecretBox::new(data),
timeout,
use_keyring,
}
)
}

pub fn store(&mut self, timeout: Duration) -> Result<&mut Self> {
store::store(&self.key_id, self.expose_secret(), timeout)?;
pub fn store(&mut self, timeout: Duration, use_keyring: bool) -> Result<&mut Self> {
self.use_keyring = use_keyring;
Self::dispatch_store(&self.key_id, self.expose_secret(), timeout, use_keyring)?;

self.timeout = timeout;
Ok(self)
}

// retrieve will fail after revoke
pub fn revoke(&mut self) -> Result<&mut Self> {
store::revoke(&self.key_id)?;
Self::dispatch_revoke(&self.key_id, self.use_keyring)?;

Ok(self)
}

#[cfg(target_os = "linux")]
fn dispatch_store(key_id: &str, secret: &[u8], timeout: Duration, use_keyring: bool) -> Result<()> {
if use_keyring {
keyring::store(key_id, secret, timeout)
} else {
memory::store(key_id, secret, timeout)
}
}

#[cfg(not(target_os = "linux"))]
fn dispatch_store(key_id: &str, secret: &[u8], timeout: Duration, _use_keyring: bool) -> Result<()> {
memory::store(key_id, secret, timeout)
}

#[cfg(target_os = "linux")]
fn dispatch_retrieve(key_id: &str, timeout: Duration, use_keyring: bool) -> Result<Box<[u8]>> {
if use_keyring {
keyring::retrieve(key_id, timeout)
} else {
memory::retrieve(key_id, timeout)
}
}

#[cfg(not(target_os = "linux"))]
fn dispatch_retrieve(key_id: &str, timeout: Duration, _use_keyring: bool) -> Result<Box<[u8]>> {
memory::retrieve(key_id, timeout)
}

#[cfg(target_os = "linux")]
fn dispatch_revoke(key_id: &str, use_keyring: bool) -> Result<()> {
if use_keyring {
keyring::revoke(key_id)
} else {
memory::revoke(key_id)
}
}

#[cfg(not(target_os = "linux"))]
fn dispatch_revoke(key_id: &str, _use_keyring: bool) -> Result<()> {
memory::revoke(key_id)
}
}

impl Deref for SecretKey {
Expand All @@ -95,8 +137,8 @@ mod tests {
#[test]
fn key_roundtrip() {
let mut key = SecretKey::new("some random string !@(as+=!#@_%$".to_string().into_bytes().into_boxed_slice());
key.store(Duration::from_secs(10)).unwrap();
let data = SecretKey::retrieve(&key.key_id, Duration::from_secs(10)).unwrap();
key.store(Duration::from_secs(10), false).unwrap();
let data = SecretKey::retrieve(&key.key_id, Duration::from_secs(10), false).unwrap();

assert_eq!(key.expose_secret(), data.expose_secret());
}
Expand All @@ -106,8 +148,8 @@ mod tests {
fn key_roundtrip_other_lengths() {
for secret in [&b"short"[..], &[7u8; 64][..]] {
let mut key = SecretKey::new(secret.to_vec().into_boxed_slice());
key.store(Duration::from_secs(10)).unwrap();
let data = SecretKey::retrieve(&key.key_id, Duration::from_secs(10)).unwrap();
key.store(Duration::from_secs(10), false).unwrap();
let data = SecretKey::retrieve(&key.key_id, Duration::from_secs(10), false).unwrap();

assert_eq!(key.expose_secret(), data.expose_secret());
}
Expand Down
18 changes: 11 additions & 7 deletions src/server/route/util.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use actix_session::Session;
use actix_web::HttpResponse;
use anyhow::{anyhow, bail};
use linux_keyutils::KeyError;
use log::{error, info};
use serde_json::json;

Expand All @@ -9,7 +10,7 @@ use crate::auth_backend::UserInfo;
use crate::config::config::Config;
use crate::keepass::db_cache::{CacheExpiredError, DbCache};
use crate::keepass::keepass::KeePass;
use crate::keepass::key::{KeyId, KeyUnavailableError, SecretKey};
use crate::keepass::key::{KeyId, SecretKey};
use crate::session::AuthSession;

pub const SESSION_KEY_KEY_ID: &str = "key_id";
Expand Down Expand Up @@ -124,7 +125,7 @@ pub(crate) async fn get_db(session: &Session, config: &Config, db_cache: &DbCach
}
);

return match err.downcast_ref::<KeyUnavailableError>() {
return match err.downcast_ref::<KeyError>() {
Some(_) => {
_close_db(session, config, db_cache).await?;

Expand Down Expand Up @@ -166,11 +167,11 @@ pub(crate) fn retrieve_key(config: &Config, session: &Session) -> anyhow::Result
let key_id = session.get::<KeyId>(SESSION_KEY_KEY_ID)?
.ok_or(anyhow!("failed to retrieve key id from session"))?;

SecretKey::retrieve(&key_id, config.db_session_timeout)
SecretKey::retrieve(&key_id, config.db_session_timeout, config.use_keyring)
}

pub(crate) fn store_key(config: &Config, session: &Session, mut key: SecretKey) -> anyhow::Result<()> {
key.store(config.db_session_timeout)?;
key.store(config.db_session_timeout, config.use_keyring)?;
session.insert(SESSION_KEY_KEY_ID, key.key_id)?;

Ok(())
Expand All @@ -196,9 +197,12 @@ pub(crate) fn revoke_key(config: &Config, session: &Session) -> anyhow::Result<(
fn check_key_err<F>(ok: F, err: anyhow::Error) -> anyhow::Result<()>
where F: Fn() -> anyhow::Result<()>
{
// Ignore non-existent, expired or already revoked
match err.downcast_ref::<KeyUnavailableError>() {
Some(_) => ok(),
match err.downcast_ref::<KeyError>() {
Some(e) => match e {
// Ignore non-existent, expired or already revoked
KeyError::KeyDoesNotExist | KeyError::KeyExpired | KeyError::KeyRevoked => ok(),
_ => Err(err),
}
None => Err(err),
}
}
8 changes: 8 additions & 0 deletions src/server/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use actix_session::{config::PersistentSession, SessionMiddleware, storage::Cooki
use actix_web::{App, HttpServer, web};
use actix_web::cookie::time::Duration;
use actix_web::middleware::{DefaultHeaders, Logger};
use log::warn;
use anyhow::Result;
use env_logger::Env;

Expand All @@ -19,6 +20,13 @@ impl Server {
let port = config.port;
env_logger::init_from_env(Env::default().default_filter_or("info"));

// the downgrade belongs in the log, not only in a config file someone
// edited once
#[cfg(target_os = "linux")]
if !config.use_keyring {
warn!("use_keyring is off: database keys are held in process memory instead of the kernel keyring");
}

let secret_key = config.session_secret_key.0.clone();
let config_data = web::Data::new(config);
let auth_cache = web::Data::new(auth_backend::new(&config_data).init().await?);
Expand Down
Loading