use std::net::IpAddr;
use std::sync::Arc;
use std::time::Duration;
use axum::extract::FromRequestParts;
use axum::http::request::Parts;
use tracing::{debug, error, info};
use crate::config::{AuditConfig, DnsConfig};
use crate::dns::{HickoryResolver, Resolver, resolver_addr};
use crate::sqlite::audit::AuditEntry;
use crate::sqlite::db::Database;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AuditEvent {
CertificateIssued,
CertificateIssueFailed,
CertificateRevoked,
CertificateRevokeFailed,
}
impl AuditEvent {
#[must_use]
pub fn as_str(&self) -> &'static str {
match self {
Self::CertificateIssued => "certificate_issued",
Self::CertificateIssueFailed => "certificate_issue_failed",
Self::CertificateRevoked => "certificate_revoked",
Self::CertificateRevokeFailed => "certificate_revoke_failed",
}
}
#[must_use]
pub fn outcome(&self) -> &'static str {
match self {
Self::CertificateIssued | Self::CertificateRevoked => "success",
Self::CertificateIssueFailed | Self::CertificateRevokeFailed => "failure",
}
}
#[must_use]
pub fn parse(value: &str) -> Option<Self> {
Some(match value {
"certificate_issued" => Self::CertificateIssued,
"certificate_issue_failed" => Self::CertificateIssueFailed,
"certificate_revoked" => Self::CertificateRevoked,
"certificate_revoke_failed" => Self::CertificateRevokeFailed,
_ => return None,
})
}
}
pub const ALL_AUDIT_EVENTS: &[AuditEvent] = &[
AuditEvent::CertificateIssued,
AuditEvent::CertificateIssueFailed,
AuditEvent::CertificateRevoked,
AuditEvent::CertificateRevokeFailed,
];
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ActorKind {
Acme,
Admin,
Cli,
System,
}
impl ActorKind {
#[must_use]
pub fn as_str(&self) -> &'static str {
match self {
Self::Acme => "acme",
Self::Admin => "admin",
Self::Cli => "cli",
Self::System => "system",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Actor {
pub kind: ActorKind,
pub id: Option<String>,
}
impl Actor {
#[must_use]
pub fn acme(account_id: impl Into<String>) -> Self {
Self {
kind: ActorKind::Acme,
id: Some(account_id.into()),
}
}
#[must_use]
pub fn acme_certificate_key() -> Self {
Self {
kind: ActorKind::Acme,
id: None,
}
}
#[must_use]
pub fn admin(username: impl Into<String>) -> Self {
Self {
kind: ActorKind::Admin,
id: Some(username.into()),
}
}
#[must_use]
pub fn cli() -> Self {
let id = std::env::var("USER")
.or_else(|_| std::env::var("LOGNAME"))
.ok()
.filter(|value| !value.is_empty());
Self {
kind: ActorKind::Cli,
id,
}
}
#[must_use]
pub fn system() -> Self {
Self {
kind: ActorKind::System,
id: None,
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct ClientContext {
pub ip: Option<String>,
pub ptr: Option<String>,
pub user_agent: Option<String>,
pub request_id: Option<String>,
}
#[derive(Debug, Clone, Default)]
pub struct RequestContext {
pub ip: Option<IpAddr>,
pub user_agent: Option<String>,
pub request_id: Option<String>,
}
const USER_AGENT_MAX: usize = 256;
impl RequestContext {
pub fn from_parts(parts: &Parts) -> Self {
Self::gather(&parts.headers, &parts.extensions)
}
pub fn from_request<B>(request: &axum::http::Request<B>) -> Self {
Self::gather(request.headers(), request.extensions())
}
fn gather(headers: &axum::http::HeaderMap, extensions: &axum::http::Extensions) -> Self {
let ip = extensions
.get::<crate::filter::ClientIp>()
.and_then(|client| client.0);
let user_agent = headers
.get(axum::http::header::USER_AGENT)
.and_then(|value| value.to_str().ok())
.map(|value| value.chars().take(USER_AGENT_MAX).collect::<String>())
.filter(|value| !value.is_empty());
let request_id = extensions
.get::<crate::middlewares::access::RequestId>()
.map(|id| id.0.clone());
Self {
ip,
user_agent,
request_id,
}
}
}
impl<S: Send + Sync> FromRequestParts<S> for RequestContext {
type Rejection = std::convert::Infallible;
async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
Ok(Self::from_parts(parts))
}
}
#[derive(Debug, Clone)]
pub struct AuditRecord {
pub event: AuditEvent,
pub profile: String,
pub actor: Actor,
pub account_id: Option<String>,
pub order_id: Option<String>,
pub cert_serial: Option<String>,
pub identifiers: Vec<String>,
pub client: ClientContext,
pub reason: Option<String>,
pub detail: Option<String>,
}
impl AuditRecord {
#[must_use]
pub fn new(event: AuditEvent, profile: impl Into<String>, actor: Actor) -> Self {
Self {
event,
profile: profile.into(),
actor,
account_id: None,
order_id: None,
cert_serial: None,
identifiers: Vec::new(),
client: ClientContext::default(),
reason: None,
detail: None,
}
}
#[must_use]
pub fn with_order(mut self, order: &crate::sqlite::order::Order) -> Self {
self.order_id = Some(order.id.clone().to_string());
self.account_id = Some(order.account_id.clone().to_string());
self.identifiers = order
.identifiers
.iter()
.map(|identifier| identifier.value.clone())
.collect();
self
}
#[must_use]
pub fn with_account(mut self, account_id: impl Into<String>) -> Self {
self.account_id = Some(account_id.into());
self
}
#[must_use]
pub fn with_serial(mut self, serial: impl Into<String>) -> Self {
self.cert_serial = Some(serial.into());
self
}
#[must_use]
pub fn with_client(mut self, client: ClientContext) -> Self {
self.client = client;
self
}
#[must_use]
pub fn with_reason(mut self, reason: impl Into<String>) -> Self {
self.reason = Some(reason.into());
self
}
#[must_use]
pub fn with_detail(mut self, detail: impl Into<String>) -> Self {
self.detail = Some(detail.into());
self
}
}
pub struct Auditor {
database: Arc<Database>,
resolver: Option<Arc<dyn Resolver>>,
ptr_timeout: Duration,
metrics: Option<Arc<crate::metrics::Metrics>>,
}
impl Auditor {
pub fn from_config(
cfg: &AuditConfig,
dns: &DnsConfig,
database: Arc<Database>,
metrics: Arc<crate::metrics::Metrics>,
) -> anyhow::Result<Self> {
let resolver: Option<Arc<dyn Resolver>> = if cfg.reverse_dns {
Some(Arc::new(match resolver_addr(dns)? {
Some(addr) => HickoryResolver::from_address(addr)
.map_err(|error| anyhow::anyhow!("audit.reverse_dns: {error}"))?,
None => HickoryResolver::from_system()
.map_err(|error| anyhow::anyhow!("audit.reverse_dns: {error}"))?,
}))
} else {
None
};
info!(
event = "audit_loaded",
outcome = "success",
reverse_dns = cfg.reverse_dns,
reverse_dns_timeout_ms = cfg.reverse_dns_timeout_ms,
retention_days = cfg.retention_days,
);
Ok(Self {
database,
resolver,
ptr_timeout: Duration::from_millis(cfg.reverse_dns_timeout_ms),
metrics: Some(metrics),
})
}
#[must_use]
pub fn with_resolver(
database: Arc<Database>,
resolver: Option<Arc<dyn Resolver>>,
ptr_timeout: Duration,
) -> Self {
Self {
database,
resolver,
ptr_timeout,
metrics: None,
}
}
pub async fn reverse(&self, ip: Option<IpAddr>) -> Option<String> {
let (resolver, ip) = (self.resolver.as_ref()?, ip?);
match tokio::time::timeout(self.ptr_timeout, resolver.reverse(ip)).await {
Ok(Ok(names)) => names.into_iter().next(),
Ok(Err(error)) => {
debug!(event = "audit_reverse_dns_failed", outcome = "failure", ip = %ip, error = %error);
None
}
Err(_) => {
debug!(
event = "audit_reverse_dns_timeout",
outcome = "failure",
ip = %ip,
timeout_ms = crate::millis(self.ptr_timeout),
);
None
}
}
}
pub async fn client(&self, request: &RequestContext) -> ClientContext {
let canonical = request.ip.map(crate::filter::canonical);
ClientContext {
ip: canonical.map(|ip| ip.to_string()),
ptr: self.reverse(canonical).await,
user_agent: request.user_agent.clone(),
request_id: request.request_id.clone(),
}
}
#[must_use]
pub fn with_metrics(mut self, metrics: Arc<crate::metrics::Metrics>) -> Self {
self.metrics = Some(metrics);
self
}
pub async fn record(&self, record: AuditRecord) {
if let Some(metrics) = &self.metrics {
metrics.record_audit(&record);
}
write(record, &self.database).await;
}
}
pub async fn write(record: AuditRecord, database: &Database) {
let (event, profile) = (record.event, record.profile.clone());
let (order_id, serial) = (record.order_id.clone(), record.cert_serial.clone());
if let Err(error) = AuditEntry::insert(record, database).await {
error!(
event = "audit_write_failed",
outcome = "failure",
audit_event = event.as_str(),
profile = %profile,
order_id = ?order_id,
cert_serial = ?serial,
error = %error,
"the action succeeded but its audit row was not written"
);
}
}
impl std::fmt::Debug for Auditor {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Auditor")
.field("reverse_dns", &self.resolver.is_some())
.field("ptr_timeout", &self.ptr_timeout)
.finish()
}
}
#[cfg(test)]
mod tests;