acme-proxy 0.5.0

An ACME (RFC 8555) server that issues from a local CA, relays to an upstream CA, or delegates to a script
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
//! The CA's audit trail: who asked this server to sign or withdraw a
//! certificate, from where, and how it ended.
//!
//! Three things live here, and they are deliberately one module rather than
//! three:
//!
//! - the **vocabulary** ([`AuditEvent`], [`Actor`], [`AuditRecord`]) every call
//!   site builds a row from;
//! - the **reverse lookup**, which is the only part that touches the network and
//!   the only part an operator can switch off (`audit.reverse_dns`);
//! - the **write**, which is best-effort by design — see [`Auditor::record`].
//!
//! ## Why this is not [`notify`](crate::notify)
//!
//! The two fire at nearly the same call sites and carry nearly the same fields,
//! which invites merging them. They answer different questions. A notification
//! is *outbound and lossy*: it goes to a chat room, it is fire-and-forget, and a
//! backend that is down loses the event with a warning. An audit row is
//! *inbound and durable*: it is the record the CA is answerable for, it is
//! queried months later by serial or by account, and it exists for the events
//! nobody wants a notification about — the refusals. Notifications also fire for
//! things that never touch the CA (`account_created`, `challenge_failed`), and
//! the audit trail records things nothing is notified about. Sharing a type
//! would mean every future field arguing about which of the two it is for.

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;

pub mod admin;

/// What this trail records: every action the CA takes on a certificate and its
/// refusal, plus every administrative action taken on the CA itself.
///
/// A refusal is an audit record in its own right. "Who tried to revoke this
/// certificate and was turned away" is the question the successes cannot
/// answer, and it is the one asked after something has gone wrong.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AuditEvent {
    // The CA acting on a certificate, and each way that is refused. These four
    // are also emitted as `tracing` events under the same names; the rest of
    // the vocabulary below is not, and `as_str` is its own authority.
    CertificateIssued,
    CertificateIssueFailed,
    CertificateRevoked,
    CertificateRevokeFailed,
    // The administration of the CA: an operator (or the host CLI) changing a
    // stored account, credential, operator, session or queue entry. Recorded
    // only on success — a refusal here is the operator being told the state of
    // things, not the CA turning a remote party away. All map to `"success"`;
    // the `outcome` match is deliberately exhaustive so a future `*_failed`
    // admin event cannot be added without classifying it.
    AccountDeactivated,
    AccountContactUpdated,
    AccountDeleted,
    OrderDeleted,
    EabCreated,
    EabRevoked,
    OperatorCreated,
    OperatorRoleChanged,
    OperatorContactUpdated,
    OperatorPasswordChanged,
    OperatorDisabled,
    OperatorEnabled,
    OperatorDeleted,
    OperatorTotpEnrolled,
    OperatorTotpDisabled,
    OperatorRecoveryCodesRegenerated,
    SessionRevoked,
    JobCancelled,
    JobAdvanced,
    NonceCleanupCompleted,
    AuditPruned,
}

impl AuditEvent {
    /// The stored form. This is the authority on the `audit_log.event`
    /// vocabulary: `20260809120000_add_audit_log.sql`'s `CHECK (event IN (…))`
    /// was dropped by a later rebuild precisely so this enum is the only place
    /// the set is defined, and [`AuditEntry::insert`] binds this, never a free
    /// string.
    #[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",
            Self::AccountDeactivated => "account_deactivated",
            Self::AccountContactUpdated => "account_contact_updated",
            Self::AccountDeleted => "account_deleted",
            Self::OrderDeleted => "order_deleted",
            Self::EabCreated => "eab_created",
            Self::EabRevoked => "eab_revoked",
            Self::OperatorCreated => "operator_created",
            Self::OperatorRoleChanged => "operator_role_changed",
            Self::OperatorContactUpdated => "operator_contact_updated",
            Self::OperatorPasswordChanged => "operator_password_changed",
            Self::OperatorDisabled => "operator_disabled",
            Self::OperatorEnabled => "operator_enabled",
            Self::OperatorDeleted => "operator_deleted",
            Self::OperatorTotpEnrolled => "operator_totp_enrolled",
            Self::OperatorTotpDisabled => "operator_totp_disabled",
            Self::OperatorRecoveryCodesRegenerated => "operator_recovery_codes_regenerated",
            Self::SessionRevoked => "session_revoked",
            Self::JobCancelled => "job_cancelled",
            Self::JobAdvanced => "job_advanced",
            Self::NonceCleanupCompleted => "nonce_cleanup_completed",
            Self::AuditPruned => "audit_pruned",
        }
    }

    /// `success` or `failure`, and the **only** definition of which is which.
    ///
    /// The column exists so "show me everything that was refused" is an index
    /// lookup rather than `event LIKE '%_failed'` written out in the CLI, the
    /// API and the page. Deriving it here rather than at each insert is what
    /// stops the two columns ever disagreeing. Exhaustive on purpose — no
    /// catch-all — so an admin `*_failed` event added later is a compile error
    /// until its author says which side it falls on.
    #[must_use]
    pub fn outcome(&self) -> &'static str {
        match self {
            Self::CertificateIssueFailed | Self::CertificateRevokeFailed => "failure",
            Self::CertificateIssued
            | Self::CertificateRevoked
            | Self::AccountDeactivated
            | Self::AccountContactUpdated
            | Self::AccountDeleted
            | Self::OrderDeleted
            | Self::EabCreated
            | Self::EabRevoked
            | Self::OperatorCreated
            | Self::OperatorRoleChanged
            | Self::OperatorContactUpdated
            | Self::OperatorPasswordChanged
            | Self::OperatorDisabled
            | Self::OperatorEnabled
            | Self::OperatorDeleted
            | Self::OperatorTotpEnrolled
            | Self::OperatorTotpDisabled
            | Self::OperatorRecoveryCodesRegenerated
            | Self::SessionRevoked
            | Self::JobCancelled
            | Self::JobAdvanced
            | Self::NonceCleanupCompleted
            | Self::AuditPruned => "success",
        }
    }

    /// Parses the stored form back. `None` for anything this enum does not
    /// define, which is also how the CLI validates `--event`.
    #[must_use]
    pub fn parse(value: &str) -> Option<Self> {
        ALL_AUDIT_EVENTS
            .iter()
            .copied()
            .find(|event| event.as_str() == value)
    }
}

/// Every [`AuditEvent`], for the CLI's `--event` help text and the page's filter.
///
/// **[`AuditEvent::parse`] is implemented over this array**, so a variant added
/// to the enum and forgotten here does not merely go unlisted: it stops
/// parsing, for ever, which means `audit list --event <name>` refuses a name the
/// server is actively writing and the page's filter cannot select it. Nothing
/// would fail — the round-trip test iterates *this* array, so it would be
/// vacuously satisfied.
///
/// `EVENT_COUNT` plus the exhaustive `match` in `event_count_is_exhaustive`
/// below is the guard: adding a variant is a compile error until both move.
/// `ALL_NOTIFY_EVENTS` keeps the same kind of assertion for the same reason.
pub const ALL_AUDIT_EVENTS: &[AuditEvent] = &[
    AuditEvent::CertificateIssued,
    AuditEvent::CertificateIssueFailed,
    AuditEvent::CertificateRevoked,
    AuditEvent::CertificateRevokeFailed,
    AuditEvent::AccountDeactivated,
    AuditEvent::AccountContactUpdated,
    AuditEvent::AccountDeleted,
    AuditEvent::OrderDeleted,
    AuditEvent::EabCreated,
    AuditEvent::EabRevoked,
    AuditEvent::OperatorCreated,
    AuditEvent::OperatorRoleChanged,
    AuditEvent::OperatorContactUpdated,
    AuditEvent::OperatorPasswordChanged,
    AuditEvent::OperatorDisabled,
    AuditEvent::OperatorEnabled,
    AuditEvent::OperatorDeleted,
    AuditEvent::OperatorTotpEnrolled,
    AuditEvent::OperatorTotpDisabled,
    AuditEvent::OperatorRecoveryCodesRegenerated,
    AuditEvent::SessionRevoked,
    AuditEvent::JobCancelled,
    AuditEvent::JobAdvanced,
    AuditEvent::NonceCleanupCompleted,
    AuditEvent::AuditPruned,
];

/// How many variants [`AuditEvent`] has, asserted against
/// [`ALL_AUDIT_EVENTS`] at compile time.
const EVENT_COUNT: usize = 25;

const _: () = assert!(
    ALL_AUDIT_EVENTS.len() == EVENT_COUNT,
    "ALL_AUDIT_EVENTS and EVENT_COUNT disagree: a variant was added to one and not the other"
);

/// Ties [`EVENT_COUNT`] to the enum itself.
///
/// An exhaustive `match` with no catch-all, so a new variant is a compile error
/// here; the arms count up to `EVENT_COUNT`, which the `const` assertion above
/// ties back to [`ALL_AUDIT_EVENTS`]. Between them, a variant cannot reach
/// production without appearing in the array [`AuditEvent::parse`] reads.
#[allow(dead_code)]
const fn event_count_is_exhaustive(event: AuditEvent) -> usize {
    match event {
        AuditEvent::CertificateIssued => 1,
        AuditEvent::CertificateIssueFailed => 2,
        AuditEvent::CertificateRevoked => 3,
        AuditEvent::CertificateRevokeFailed => 4,
        AuditEvent::AccountDeactivated => 5,
        AuditEvent::AccountContactUpdated => 6,
        AuditEvent::AccountDeleted => 7,
        AuditEvent::OrderDeleted => 8,
        AuditEvent::EabCreated => 9,
        AuditEvent::EabRevoked => 10,
        AuditEvent::OperatorCreated => 11,
        AuditEvent::OperatorRoleChanged => 12,
        AuditEvent::OperatorContactUpdated => 13,
        AuditEvent::OperatorPasswordChanged => 14,
        AuditEvent::OperatorDisabled => 15,
        AuditEvent::OperatorEnabled => 16,
        AuditEvent::OperatorDeleted => 17,
        AuditEvent::OperatorTotpEnrolled => 18,
        AuditEvent::OperatorTotpDisabled => 19,
        AuditEvent::OperatorRecoveryCodesRegenerated => 20,
        AuditEvent::SessionRevoked => 21,
        AuditEvent::JobCancelled => 22,
        AuditEvent::JobAdvanced => 23,
        AuditEvent::NonceCleanupCompleted => 24,
        AuditEvent::AuditPruned => EVENT_COUNT,
    }
}

/// Which front end acted.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ActorKind {
    /// A certificate client over the ACME API.
    Acme,
    /// An operator through the web admin.
    Admin,
    /// `acme-proxy order revoke` on the host.
    Cli,
    /// The `relay` signer's background task, settling an issuance this
    /// server already answered `processing`. The one actor with no request
    /// behind it, and therefore no address — see [`Actor::system`].
    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",
        }
    }
}

/// Who acted, and their identity within that kind.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Actor {
    pub kind: ActorKind,
    pub id: Option<String>,
}

impl Actor {
    /// An ACME client acting as a known account.
    #[must_use]
    pub fn acme(account_id: impl Into<String>) -> Self {
        Self {
            kind: ActorKind::Acme,
            id: Some(account_id.into()),
        }
    }

    /// An ACME client that proved possession of the certificate's own key pair
    /// and named no account — RFC 8555 §7.6's accountless revocation.
    ///
    /// The `None` is the honest answer and not a gap: there is no identity to
    /// record beyond "whoever holds this certificate's private key", which the
    /// `cert_serial` on the same row already says.
    #[must_use]
    pub fn acme_certificate_key() -> Self {
        Self {
            kind: ActorKind::Acme,
            id: None,
        }
    }

    /// An operator signed in to the web admin.
    #[must_use]
    pub fn admin(username: impl Into<String>) -> Self {
        Self {
            kind: ActorKind::Admin,
            id: Some(username.into()),
        }
    }

    /// The command line, identified by whichever of `$USER`/`$LOGNAME` is set.
    ///
    /// Advisory only, and unavoidably so: anything running this binary can set
    /// those variables. It narrows "somebody on the host" to "somebody on the
    /// host, probably this account", which is the most a process can say about
    /// its own invoker without help from the audit subsystem of the OS.
    #[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,
        }
    }

    /// This server's own background work.
    #[must_use]
    pub fn system() -> Self {
        Self {
            kind: ActorKind::System,
            id: None,
        }
    }
}

/// The request a row came from: address, its reverse name, and the two headers
/// worth keeping.
///
/// Entirely empty for [`ActorKind::Cli`] and [`ActorKind::System`], which is
/// why every field is optional rather than a placeholder string — "there was no
/// client" and "the client sent no User-Agent" are both `None`, and the
/// `actor_kind` on the row already tells them apart.
#[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>,
}

/// What a request carries before the reverse lookup has run.
///
/// An extractor rather than three `Extension`s at each call site: a handler
/// that records an audit row wants all of this or none of it, and gathering it
/// in one place is what keeps `User-Agent`'s truncation rule (below) from being
/// re-decided per handler. Resolve it into a [`ClientContext`] with
/// [`Auditor::client`].
#[derive(Debug, Clone, Default)]
pub struct RequestContext {
    pub ip: Option<IpAddr>,
    pub user_agent: Option<String>,
    pub request_id: Option<String>,
}

/// Longest `User-Agent` kept. Real ones are well under this; the header is
/// attacker-controlled and ends up in a database column and an HTML page, so it
/// gets a ceiling rather than trust.
///
/// `pub(crate)` for `webadmin::user_agent_of`, which caps the *same* header on
/// the way into a notification payload. One constant, so the two answers to
/// "how much of this do we keep?" cannot drift.
pub(crate) const USER_AGENT_MAX: usize = 256;

impl RequestContext {
    /// Reads the address the filter middleware resolved, plus the two headers.
    ///
    /// Free of the request body, so this composes with `AcmeRequest<T>` — which
    /// consumes it — in the usual axum order.
    pub fn from_parts(parts: &Parts) -> Self {
        Self::gather(&parts.headers, &parts.extensions)
    }

    /// Same, from a whole request.
    ///
    /// `verify_jws` needs this: it is handed the `Request` and consumes it into
    /// a body string, so it has to read the context *before* the point where
    /// the extractor machinery would hand it `Parts`.
    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))
    }
}

/// One row, before it is written.
///
/// Built with [`AuditRecord::new`] plus the `with_*` setters rather than a
/// struct literal: the four events populate different subsets — an issuance
/// failure has no serial, an accountless revocation has no account — and a
/// literal would mean a column of `None`s at every call site.
#[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,
        }
    }

    /// A record for an administrative action that is not scoped to one ACME
    /// endpoint — an operator, a session, the nonce table, the audit log
    /// itself. `profile` is stored empty (the column stays `NOT NULL`; `""`
    /// reads as "no profile"), and the acting identity is [`Actor`] plus
    /// whatever [`AuditRecord::with_detail`] carries. Account, EAB and order
    /// actions keep [`AuditRecord::new`] with their real profile.
    #[must_use]
    pub fn admin(event: AuditEvent, actor: Actor) -> Self {
        Self::new(event, String::new(), actor)
    }

    /// Fills in the subject from the order: its id, its account and the names
    /// it covers, the last frozen into the row rather than joined back — the
    /// order may be deleted long before the row is read.
    #[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
    }

    /// The RFC 8555 problem type on a refusal, or the RFC 5280 reason code on a
    /// revocation. Never both — see the column comment in the migration.
    #[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
    }
}

/// Writes audit rows, and resolves the reverse names that go in them.
///
/// One per process, shared by the ACME listener ([`crate::AppState`]), the web
/// admin ([`crate::webadmin::AdminState`]) and the CLI. Process-wide because
/// `[audit]` is: the trail describes the CA, not one of its endpoints.
pub struct Auditor {
    database: Arc<Database>,
    /// `None` when `audit.reverse_dns` is off, which is what makes the switch
    /// structural: there is no resolver to call rather than a boolean checked
    /// at each call site. Same shape as `ChallengeRegistry`'s bypass flag
    /// refusing to *construct* the validators.
    resolver: Option<Arc<dyn Resolver>>,
    ptr_timeout: Duration,
    /// The process's Prometheus counters.
    ///
    /// `None` only for an auditor built through [`Auditor::with_resolver`],
    /// which is test scaffolding. The serving path goes through
    /// [`Auditor::from_config`], where it is a **required argument** rather
    /// than a builder step — see that constructor.
    metrics: Option<Arc<crate::metrics::Metrics>>,
}

impl Auditor {
    /// Builds the auditor, and with it the **cached** resolver its PTR lookups
    /// go through.
    ///
    /// Cached, unlike the shared resolver `Profile::build_all` threads through
    /// the challenge and signer subsystems, and for the reason
    /// `filter::reverse_dns` makes the same choice: a PTR record for an address
    /// that keeps connecting is exactly what a cache is for, and there is no
    /// just-published-record problem here — the answer being a few minutes old
    /// is not a failure mode for a column that says "the name this address had
    /// at the time".
    ///
    /// A second cached resolver rather than sharing `reverse_dns`'s: that one
    /// is per-profile and built only when the filter is enabled, and reaching
    /// across for it would tie the audit trail's completeness to whether an
    /// unrelated filter happens to be switched on.
    /// `metrics` is a required argument and deliberately not a builder step.
    /// It was one, briefly, and the omission it invited happened immediately:
    /// the serving path built its auditor without ever calling the builder, so
    /// `acme_proxy_certificates_issued_total` stayed at zero in production
    /// while every test passed — the test harness wired the registry itself, so
    /// what the tests proved was the harness's wiring and not the server's. A
    /// parameter cannot be forgotten. Test scaffolding that genuinely has no
    /// registry uses [`Auditor::with_resolver`] instead.
    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),
        })
    }

    /// Same, against a caller-supplied resolver — or none, for the reverse
    /// lookup switched off. Used by tests and by [`Self::from_config`].
    #[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,
        }
    }

    /// The reverse name for `ip`, or `None`.
    ///
    /// Every failure is `None`: no PTR record, a resolver that timed out, a
    /// SERVFAIL, `audit.reverse_dns` off, or no client address at all. Nothing
    /// downstream distinguishes them, because nothing downstream *authorises*
    /// on this value — it is a label on a row, and a label that is sometimes
    /// missing is worth more than a request that failed to get one.
    ///
    /// The first name only when several PTR records answer. Storing all of them
    /// would make the column a list nothing queries; `filter.reverse_dns` is
    /// where multiple candidates genuinely matter, and it looks them up itself.
    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
            }
        }
    }

    /// Resolves a [`RequestContext`] into the [`ClientContext`] a row stores,
    /// running the reverse lookup on the way.
    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(),
        }
    }

    /// Attaches the Prometheus registry to an auditor built by
    /// [`Auditor::with_resolver`].
    ///
    /// Exists for the test harness, which builds its auditor with a stub
    /// resolver and still wants the counters. The serving path does **not** use
    /// this — [`Auditor::from_config`] takes the registry as a parameter, so it
    /// cannot be left off.
    #[must_use]
    pub fn with_metrics(mut self, metrics: Arc<crate::metrics::Metrics>) -> Self {
        self.metrics = Some(metrics);
        self
    }

    /// The registry this auditor counts into, for the one caller that writes a
    /// record through the free [`write()`] rather than through [`Auditor::record`]
    /// and so has to carry the counter itself.
    ///
    /// That caller is `signer::relay::abandon_relayed_order`, which is shared
    /// with a background task holding no `Auditor` at all — see its own note on
    /// why the count is spelled out there. Handing the registry over keeps the
    /// operator-cancel path counting into the same place the runner's does, so
    /// `/metrics` and `acme-proxy audit list` cannot disagree about how many
    /// issuances failed.
    #[must_use]
    pub fn metrics(&self) -> Option<&Arc<crate::metrics::Metrics>> {
        self.metrics.as_ref()
    }

    /// Writes one row, and counts it.
    ///
    /// The counter is driven off the *same* [`AuditRecord`] that is about to be
    /// stored, which is what makes "how many certificates did we issue" answer
    /// identically whether it is asked of the metrics endpoint or of
    /// `acme-proxy audit list`. A second set of call sites incrementing
    /// counters beside the audit writes would have been free to drift.
    ///
    /// See [`write()`], which this is the stateful spelling of.
    pub async fn record(&self, record: AuditRecord) {
        if let Some(metrics) = &self.metrics {
            metrics.record_audit(&record);
        }
        write(record, &self.database).await;
    }
}

/// Writes one row against a bare database handle.
///
/// The free function exists for the `relay` backend: it settles an issuance
/// from a background task that holds an `Arc<Database>` and no [`Auditor`], and
/// it needs no reverse lookup either — the address it records was resolved
/// during the finalize request and stored on the `upstream_orders` row. Giving
/// that task an `Auditor` would have meant threading one through
/// `signer::build_backends` and `Profile::build_all` for the sake of a resolver
/// it would never call.
///
/// **A failed write is logged and swallowed.** The alternative — failing the
/// request — would turn a certificate this CA has already signed into a 500 the
/// client retries, issuing a second one, which is a worse outcome for the same
/// underlying fault. It is also nearly unreachable in practice: this is the
/// same SQLite file the order was just written to, so a failure here means the
/// write that preceded it had already failed. The `error!` carries the record's
/// identifying fields, so the trail survives in the log even when the table did
/// not get it.
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 {
    /// Renders the configured policy. `dyn Resolver` is not `Debug`, so the
    /// resolver shows as whether there is one — which is the whole of what it
    /// contributes to behaviour here.
    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;