Skip to main content

acme_proxy/filter/
identifiers.rs

1//! The `identifiers` filter: which names a client may have certified.
2//!
3//! Runs twice per order — against the `newOrder` identifiers, then against the
4//! names the finalize CSR actually requests. The first check is the useful
5//! error (the client learns immediately, before doing the challenge dance); the
6//! second is the one that is load-bearing, because the CSR is what gets signed.
7//!
8//! ## Types matter as much as values
9//!
10//! A CSR is not a list of hostnames. Its subject alternative names can be IP
11//! addresses, email addresses or URIs, and its subject can carry a common name
12//! that no SAN mentions. All of them are projected into the same
13//! [`Identifier`](crate::sqlite::order::Identifier) list by the caller (see
14//! `csr_identifiers` in [`crate::handlers`]), so a
15//! `deny` pattern cannot be sidestepped by moving a name from a DNS SAN to the
16//! common name, or from a DNS SAN to an IP SAN.
17//!
18//! `allowed_types` is the front door for that: it defaults to `["dns", "cn"]`,
19//! so a CSR asking for an IP address is refused outright rather than being
20//! matched against hostname regexes that were never written with addresses in
21//! mind.
22//!
23//! ## Why `allow` and `deny` have different reach
24//!
25//! `deny` applies to **every** projected identifier, the common name included.
26//! It means "never certify this string", and the broadest possible reading is
27//! the useful one — otherwise a denied name could ride along in the subject.
28//!
29//! `allow` applies only to the identifiers a certificate is actually *for*,
30//! i.e. those derived from subject alternative names. The common name is legacy
31//! subject metadata rather than a name being certified (RFC 6125 deprecated
32//! relying on it, and it is only honoured at all when no SAN is present), and
33//! real-world CSR generators routinely put a human label there —
34//! `rcgen`'s own default is the string `rcgen self signed cert`. Requiring that
35//! to match a list of domain patterns would reject perfectly valid requests
36//! without making anything safer, because the SANs are separately constrained.
37//!
38//! ## Why wildcards are refused by default
39//!
40//! Patterns are anchored, so a `deny` entry of `secret\.example\.com` does *not*
41//! match the string `*.example.com` — yet a certificate for that wildcard covers
42//! `secret.example.com` perfectly well. An operator relying on a deny list would
43//! find it silently widened the day `dns-01` was enabled and wildcards became
44//! orderable at all.
45//!
46//! `allow_wildcards` therefore defaults to `false`: a `dns` value beginning
47//! `*.` is refused before the lists are consulted. Turning it on is a statement
48//! that the patterns were written with the wildcard form in mind.
49//!
50//! ## Globs first, regexes on request
51//!
52//! `allow`/`deny` take globs (`*.example.com`), where `*` matches one label —
53//! the wildcard semantics an operator already knows from certificates.
54//! `allow_regex`/`deny_regex` take the anchored regexes this check used to
55//! require. The two are unioned, so a policy can be mostly globs with one
56//! regex where a glob will not do. Regex is the sharper tool and the anchoring
57//! footgun above is why it is no longer the only one.
58
59use std::collections::BTreeSet;
60
61use async_trait::async_trait;
62use regex::Regex;
63use tracing::info;
64
65use super::policy::{Check, StageSet, Verdict};
66use super::{
67    IdentifierContext, ListVerdict, SUBJECT_ONLY_TYPES, check_lists, compile_matchers,
68    default_identifier_types,
69};
70
71/// Resolved `[filter.check.<name>]` settings for `type = "identifiers"`.
72#[derive(Debug, Clone)]
73pub struct Settings {
74    pub allowed_types: Vec<String>,
75    pub allow: Vec<String>,
76    pub deny: Vec<String>,
77    pub allow_regex: Vec<String>,
78    pub deny_regex: Vec<String>,
79    pub allow_wildcards: bool,
80}
81
82impl Default for Settings {
83    fn default() -> Self {
84        Self {
85            allowed_types: default_identifier_types(),
86            allow: Vec::new(),
87            deny: Vec::new(),
88            allow_regex: Vec::new(),
89            deny_regex: Vec::new(),
90            allow_wildcards: false,
91        }
92    }
93}
94
95/// Accepts or refuses each requested name by type, then by pattern.
96#[derive(Debug)]
97pub struct IdentifierList {
98    allowed_types: BTreeSet<String>,
99    allow: Vec<Regex>,
100    deny: Vec<Regex>,
101    allow_wildcards: bool,
102}
103
104impl IdentifierList {
105    /// Compiles the patterns, failing startup on a bad one.
106    pub fn from_settings(name: &str, settings: &Settings) -> anyhow::Result<Self> {
107        let check = Self {
108            allowed_types: settings
109                .allowed_types
110                .iter()
111                .map(|typ| typ.to_ascii_lowercase())
112                .collect(),
113            allow: compile_matchers(&settings.allow, &settings.allow_regex, name, "allow")?,
114            deny: compile_matchers(&settings.deny, &settings.deny_regex, name, "deny")?,
115            allow_wildcards: settings.allow_wildcards,
116        };
117        info!(
118            event = "filter_identifiers_loaded",
119            outcome = "success",
120            check = name,
121            allowed_types = ?settings.allowed_types,
122            allow = check.allow.len(),
123            deny = check.deny.len(),
124            allow_wildcards = settings.allow_wildcards,
125        );
126        Ok(check)
127    }
128
129    fn decide(&self, context: &IdentifierContext<'_>) -> Verdict {
130        let stage = context.stage.as_str();
131
132        for identifier in context.identifiers {
133            let typ = identifier.typ.to_ascii_lowercase();
134            if !self.allowed_types.contains(&typ) {
135                return Verdict::Fail(format!(
136                    "{stage} requests a {} identifier, which is not permitted",
137                    identifier.typ
138                ));
139            }
140
141            let value = &identifier.value;
142
143            // A wildcard covers every name under a domain, but reads to an
144            // anchored pattern as the literal string `*.example.com` — so it
145            // slips past a deny rule for a name it then certifies. Refused
146            // before the lists are consulted, unless the operator has said the
147            // patterns account for it.
148            if !self.allow_wildcards && typ == "dns" && value.starts_with("*.") {
149                return Verdict::Fail(format!(
150                    "{stage} identifier {value} is a wildcard, which policy does not permit"
151                ));
152            }
153
154            // Deny reaches everything, including the subject common name, and
155            // wins over allow: an operator who lists something explicitly means
156            // it, even if a broad allow entry would also have matched. Allow, by
157            // contrast, constrains only the names the certificate is issued
158            // *for*, so a subject-only type skips it — hence the empty slice.
159            let allow: &[Regex] = if SUBJECT_ONLY_TYPES.contains(&typ.as_str()) {
160                &[]
161            } else {
162                &self.allow
163            };
164
165            match check_lists(allow, &self.deny, |pattern| pattern.is_match(value)) {
166                ListVerdict::Permitted => {}
167                ListVerdict::Denied => {
168                    return Verdict::Fail(format!(
169                        "{stage} identifier {value} is denied by policy"
170                    ));
171                }
172                ListVerdict::NotAllowed => {
173                    return Verdict::Fail(format!(
174                        "{stage} identifier {value} is not permitted by policy"
175                    ));
176                }
177            }
178        }
179
180        Verdict::Pass
181    }
182}
183
184#[async_trait]
185impl Check for IdentifierList {
186    fn kind(&self) -> &'static str {
187        "identifiers"
188    }
189
190    fn stages(&self) -> StageSet {
191        StageSet::identifiers_only()
192    }
193
194    async fn check_identifiers(&self, context: &IdentifierContext<'_>) -> Verdict {
195        self.decide(context)
196    }
197}
198
199#[cfg(test)]
200mod tests {
201    use super::*;
202    use crate::filter::IdentifierStage;
203    use crate::sqlite::order::Identifier;
204    use crate::testutil::identifiers as ids;
205
206    fn globs(allow: &[&str], deny: &[&str]) -> Settings {
207        Settings {
208            allow: allow.iter().map(std::string::ToString::to_string).collect(),
209            deny: deny.iter().map(std::string::ToString::to_string).collect(),
210            ..Settings::default()
211        }
212    }
213
214    fn regexes(allow: &[&str], deny: &[&str]) -> Settings {
215        Settings {
216            allow_regex: allow.iter().map(std::string::ToString::to_string).collect(),
217            deny_regex: deny.iter().map(std::string::ToString::to_string).collect(),
218            ..Settings::default()
219        }
220    }
221
222    fn built(settings: &Settings) -> IdentifierList {
223        IdentifierList::from_settings("names", settings).unwrap()
224    }
225
226    async fn verdict_for(
227        check: &IdentifierList,
228        stage: IdentifierStage,
229        identifiers: &[Identifier],
230    ) -> Verdict {
231        check
232            .check_identifiers(&IdentifierContext {
233                client_ip: None,
234                account_id: "acct-1",
235                stage,
236                identifiers,
237
238                eab: None,
239            })
240            .await
241    }
242
243    fn assert_failed(verdict: Verdict, needle: &str) {
244        match verdict {
245            Verdict::Fail(detail) => {
246                assert!(detail.contains(needle), "{detail:?} lacks {needle:?}");
247            }
248            other => panic!("expected Fail, got {other:?}"),
249        }
250    }
251
252    // ---- globs, the default spelling --------------------------------------
253
254    /// `*` is one label, matching the wildcard semantics of a certificate —
255    /// which is the whole reason globs are the default and regexes the opt-in.
256    #[tokio::test]
257    async fn a_glob_star_matches_exactly_one_label() {
258        let check = built(&globs(&["*.example.com"], &[]));
259
260        assert_eq!(
261            verdict_for(
262                &check,
263                IdentifierStage::NewOrder,
264                &ids(&[("dns", "a.example.com")])
265            )
266            .await,
267            Verdict::Pass
268        );
269        assert_failed(
270            verdict_for(
271                &check,
272                IdentifierStage::NewOrder,
273                &ids(&[("dns", "a.b.example.com")]),
274            )
275            .await,
276            "not permitted by policy",
277        );
278        // The bare name is a separate entry, exactly as in a certificate.
279        assert_failed(
280            verdict_for(
281                &check,
282                IdentifierStage::NewOrder,
283                &ids(&[("dns", "example.com")]),
284            )
285            .await,
286            "not permitted by policy",
287        );
288    }
289
290    #[tokio::test]
291    async fn listing_the_bare_name_beside_the_glob_covers_both() {
292        let check = built(&globs(&["*.example.com", "example.com"], &[]));
293        for name in ["a.example.com", "example.com"] {
294            assert_eq!(
295                verdict_for(&check, IdentifierStage::NewOrder, &ids(&[("dns", name)])).await,
296                Verdict::Pass,
297                "{name}"
298            );
299        }
300    }
301
302    #[tokio::test]
303    async fn a_glob_ignores_case_and_a_trailing_dot_is_not_special() {
304        let check = built(&globs(&["*.example.com"], &[]));
305        assert_eq!(
306            verdict_for(
307                &check,
308                IdentifierStage::NewOrder,
309                &ids(&[("dns", "A.Example.COM")])
310            )
311            .await,
312            Verdict::Pass
313        );
314    }
315
316    /// Everything that is not `*` is literal, so a name that happens to hold
317    /// regex metacharacters cannot smuggle a pattern in.
318    #[tokio::test]
319    async fn a_glob_escapes_every_other_metacharacter() {
320        let check = built(&globs(&["a.example.com"], &[]));
321        // `.` must not match an arbitrary character.
322        assert_failed(
323            verdict_for(
324                &check,
325                IdentifierStage::NewOrder,
326                &ids(&[("dns", "axexample.com")]),
327            )
328            .await,
329            "not permitted",
330        );
331    }
332
333    #[tokio::test]
334    async fn a_glob_deny_wins_over_a_glob_allow() {
335        let check = built(&globs(&["*.example.com"], &["secret.example.com"]));
336        assert_failed(
337            verdict_for(
338                &check,
339                IdentifierStage::NewOrder,
340                &ids(&[("dns", "secret.example.com")]),
341            )
342            .await,
343            "is denied by policy",
344        );
345    }
346
347    /// The two spellings are one list each, so a policy can be mostly globs
348    /// with one regex where a glob will not do.
349    #[tokio::test]
350    async fn globs_and_regexes_are_unioned_on_both_sides() {
351        let settings = Settings {
352            allow: vec!["*.example.com".to_string()],
353            allow_regex: vec![r"host\d+\.internal".to_string()],
354            deny: vec!["secret.example.com".to_string()],
355            deny_regex: vec![r"host666\.internal".to_string()],
356            ..Settings::default()
357        };
358        let check = built(&settings);
359
360        for permitted in ["a.example.com", "host12.internal"] {
361            assert_eq!(
362                verdict_for(
363                    &check,
364                    IdentifierStage::NewOrder,
365                    &ids(&[("dns", permitted)])
366                )
367                .await,
368                Verdict::Pass,
369                "{permitted}"
370            );
371        }
372        for refused in ["secret.example.com", "host666.internal"] {
373            assert_failed(
374                verdict_for(&check, IdentifierStage::NewOrder, &ids(&[("dns", refused)])).await,
375                "is denied by policy",
376            );
377        }
378    }
379
380    #[tokio::test]
381    async fn a_glob_allow_skips_the_common_name_but_a_glob_deny_reaches_it() {
382        let check = built(&globs(&["*.example.com"], &["secret.example.com"]));
383
384        // `allow` does not constrain subject metadata...
385        assert_eq!(
386            verdict_for(
387                &check,
388                IdentifierStage::Csr,
389                &ids(&[("dns", "ok.example.com"), ("cn", "rcgen self signed cert")])
390            )
391            .await,
392            Verdict::Pass
393        );
394        // ...but `deny` does.
395        assert_failed(
396            verdict_for(
397                &check,
398                IdentifierStage::Csr,
399                &ids(&[("dns", "ok.example.com"), ("cn", "secret.example.com")]),
400            )
401            .await,
402            "is denied by policy",
403        );
404    }
405
406    // ---- the list semantics -----------------------------------------------
407
408    #[tokio::test]
409    async fn an_empty_allow_list_permits_any_allowed_type() {
410        let check = built(&Settings::default());
411        let identifiers = ids(&[("dns", "anything.example.com"), ("cn", "other.test")]);
412        assert_eq!(
413            verdict_for(&check, IdentifierStage::NewOrder, &identifiers).await,
414            Verdict::Pass
415        );
416    }
417
418    #[tokio::test]
419    async fn the_allow_list_refuses_everything_else() {
420        let check = built(&regexes(&[r".*\.example\.com"], &[]));
421        assert_eq!(
422            verdict_for(
423                &check,
424                IdentifierStage::NewOrder,
425                &ids(&[("dns", "a.example.com")])
426            )
427            .await,
428            Verdict::Pass
429        );
430        assert_failed(
431            verdict_for(
432                &check,
433                IdentifierStage::NewOrder,
434                &ids(&[("dns", "a.evil.net")]),
435            )
436            .await,
437            "not permitted by policy",
438        );
439    }
440
441    #[tokio::test]
442    async fn deny_wins_over_allow() {
443        let check = built(&regexes(&[r".*\.example\.com"], &[r"secret\..*"]));
444        assert_failed(
445            verdict_for(
446                &check,
447                IdentifierStage::NewOrder,
448                &ids(&[("dns", "secret.example.com")]),
449            )
450            .await,
451            "is denied by policy",
452        );
453    }
454
455    #[tokio::test]
456    async fn patterns_are_anchored_so_a_suffix_cannot_bypass_them() {
457        let check = built(&regexes(&[r"example\.com"], &[]));
458        // Unanchored, this would have been accepted.
459        assert_failed(
460            verdict_for(
461                &check,
462                IdentifierStage::NewOrder,
463                &ids(&[("dns", "example.com.evil.net")]),
464            )
465            .await,
466            "not permitted",
467        );
468    }
469
470    #[tokio::test]
471    async fn matching_ignores_case() {
472        let check = built(&regexes(&[], &[r"secret\.example\.com"]));
473        assert_failed(
474            verdict_for(
475                &check,
476                IdentifierStage::NewOrder,
477                &ids(&[("dns", "SECRET.Example.COM")]),
478            )
479            .await,
480            "is denied",
481        );
482    }
483
484    #[tokio::test]
485    async fn an_ip_san_is_refused_by_the_default_allowed_types() {
486        let check = built(&Settings::default());
487        assert_failed(
488            verdict_for(
489                &check,
490                IdentifierStage::Csr,
491                &ids(&[("dns", "ok.example.com"), ("ip", "10.0.0.1")]),
492            )
493            .await,
494            "requests a ip identifier",
495        );
496    }
497
498    #[tokio::test]
499    async fn email_and_uri_sans_are_refused_too() {
500        let check = built(&Settings::default());
501        for identifier in [("email", "a@example.com"), ("uri", "https://example.com")] {
502            assert_failed(
503                verdict_for(&check, IdentifierStage::Csr, &ids(&[identifier])).await,
504                "is not permitted",
505            );
506        }
507    }
508
509    #[tokio::test]
510    async fn an_operator_can_opt_into_ip_identifiers() {
511        let settings = Settings {
512            allowed_types: vec!["dns".to_string(), "ip".to_string()],
513            allow_regex: vec![r"10\..*".to_string(), r".*\.example\.com".to_string()],
514            ..Settings::default()
515        };
516        let check = built(&settings);
517        assert_eq!(
518            verdict_for(
519                &check,
520                IdentifierStage::Csr,
521                &ids(&[("ip", "10.0.0.1"), ("dns", "a.example.com")])
522            )
523            .await,
524            Verdict::Pass
525        );
526    }
527
528    // ---- wildcards --------------------------------------------------------
529
530    /// The regression this guards: patterns are anchored, so a deny rule for
531    /// `secret.example.com` never matches the string `*.example.com` — while the
532    /// certificate it yields covers that name. Enabling `dns-01` must not widen
533    /// an existing list by itself.
534    #[tokio::test]
535    async fn a_wildcard_is_refused_by_default_even_when_the_lists_would_permit_it() {
536        let check = built(&regexes(&[], &[r"secret\.example\.com"]));
537
538        // The deny pattern does not match the wildcard string...
539        assert_failed(
540            verdict_for(
541                &check,
542                IdentifierStage::NewOrder,
543                &ids(&[("dns", "*.example.com")]),
544            )
545            .await,
546            "is a wildcard",
547        );
548        // ...and the name it would cover is still denied on its own.
549        assert_failed(
550            verdict_for(
551                &check,
552                IdentifierStage::NewOrder,
553                &ids(&[("dns", "secret.example.com")]),
554            )
555            .await,
556            "denied by policy",
557        );
558    }
559
560    /// Opting in puts wildcards back under the ordinary list rules — including
561    /// `deny`, which an operator can then write against the `*.` form.
562    #[tokio::test]
563    async fn an_operator_can_opt_into_wildcards() {
564        let settings = Settings {
565            allow_regex: vec![r"\*\.example\.com".to_string()],
566            deny_regex: vec![r"\*\.internal\.example".to_string()],
567            allow_wildcards: true,
568            ..Settings::default()
569        };
570        let check = built(&settings);
571
572        assert_eq!(
573            verdict_for(
574                &check,
575                IdentifierStage::NewOrder,
576                &ids(&[("dns", "*.example.com")])
577            )
578            .await,
579            Verdict::Pass
580        );
581        assert_failed(
582            verdict_for(
583                &check,
584                IdentifierStage::NewOrder,
585                &ids(&[("dns", "*.internal.example")]),
586            )
587            .await,
588            "denied by policy",
589        );
590    }
591
592    /// A glob is literal outside `*`, so the wildcard form can be listed as a
593    /// glob too once `allow_wildcards` is on.
594    #[tokio::test]
595    async fn a_glob_can_name_the_wildcard_form_itself() {
596        let settings = Settings {
597            allow: vec!["*.example.com".to_string()],
598            allow_wildcards: true,
599            ..Settings::default()
600        };
601        let check = built(&settings);
602        // `*` in the glob matches the literal `*` label of the identifier.
603        assert_eq!(
604            verdict_for(
605                &check,
606                IdentifierStage::NewOrder,
607                &ids(&[("dns", "*.example.com")])
608            )
609            .await,
610            Verdict::Pass
611        );
612    }
613
614    /// The wildcard check is about `dns` values, not about a literal asterisk
615    /// anywhere: a common name is subject metadata and may well be a label.
616    #[tokio::test]
617    async fn the_wildcard_check_does_not_reach_the_common_name() {
618        let check = built(&Settings::default());
619        assert_eq!(
620            verdict_for(
621                &check,
622                IdentifierStage::Csr,
623                &ids(&[("dns", "example.com"), ("cn", "*.example.com")])
624            )
625            .await,
626            Verdict::Pass
627        );
628    }
629
630    // ---- allow versus deny reach ------------------------------------------
631
632    #[tokio::test]
633    async fn deny_reaches_the_common_name() {
634        // The bypass this closes: a benign SAN with the real target in the CN.
635        let check = built(&regexes(&[], &[r"secret\.example\.com"]));
636        assert_failed(
637            verdict_for(
638                &check,
639                IdentifierStage::Csr,
640                &ids(&[("dns", "ok.example.com"), ("cn", "secret.example.com")]),
641            )
642            .await,
643            "is denied by policy",
644        );
645    }
646
647    /// `allow` lists domains, but a common name is frequently a human label —
648    /// `rcgen`'s own default is `rcgen self signed cert`. Holding the CN to a
649    /// domain allowlist would reject valid requests without gaining anything,
650    /// since the SANs are constrained separately.
651    #[tokio::test]
652    async fn allow_does_not_constrain_the_common_name() {
653        let check = built(&regexes(&[r".*\.example\.com"], &[]));
654        assert_eq!(
655            verdict_for(
656                &check,
657                IdentifierStage::Csr,
658                &ids(&[("dns", "ok.example.com"), ("cn", "rcgen self signed cert")])
659            )
660            .await,
661            Verdict::Pass
662        );
663    }
664
665    /// …but the SANs still are, even when the CN would have satisfied the list.
666    #[tokio::test]
667    async fn allow_still_constrains_the_sans() {
668        let check = built(&regexes(&[r".*\.example\.com"], &[]));
669        assert_failed(
670            verdict_for(
671                &check,
672                IdentifierStage::Csr,
673                &ids(&[("dns", "evil.net"), ("cn", "ok.example.com")]),
674            )
675            .await,
676            "evil.net is not permitted",
677        );
678    }
679
680    /// A common name is still subject to `allowed_types`.
681    #[tokio::test]
682    async fn the_common_name_can_be_disallowed_by_type() {
683        let settings = Settings {
684            allowed_types: vec!["dns".to_string()],
685            ..Settings::default()
686        };
687        let check = built(&settings);
688        assert_failed(
689            verdict_for(&check, IdentifierStage::Csr, &ids(&[("cn", "anything")])).await,
690            "requests a cn identifier",
691        );
692    }
693
694    #[tokio::test]
695    async fn the_stage_appears_in_the_detail() {
696        let check = built(&regexes(&[], &[r".*"]));
697        assert_failed(
698            verdict_for(
699                &check,
700                IdentifierStage::NewOrder,
701                &ids(&[("dns", "a.example.com")]),
702            )
703            .await,
704            "newOrder identifier",
705        );
706        assert_failed(
707            verdict_for(
708                &check,
709                IdentifierStage::Csr,
710                &ids(&[("dns", "a.example.com")]),
711            )
712            .await,
713            "CSR identifier",
714        );
715    }
716
717    #[tokio::test]
718    async fn an_empty_identifier_list_is_allowed() {
719        let check = built(&regexes(&["nothing"], &[]));
720        assert_eq!(
721            verdict_for(&check, IdentifierStage::Csr, &[]).await,
722            Verdict::Pass
723        );
724    }
725
726    // ---- startup ----------------------------------------------------------
727
728    #[test]
729    fn a_bad_pattern_is_a_startup_error() {
730        let error = IdentifierList::from_settings("names", &regexes(&[], &["[unclosed"]))
731            .unwrap_err()
732            .to_string();
733        assert!(error.contains("filter.check.names.deny_regex"), "{error}");
734    }
735
736    #[test]
737    fn reports_its_type_and_stages() {
738        let check = built(&Settings::default());
739        assert_eq!(check.kind(), "identifiers");
740        assert_eq!(check.stages(), StageSet::identifiers_only());
741    }
742
743    #[test]
744    fn the_default_settings_permit_dns_names_and_common_names() {
745        assert_eq!(Settings::default().allowed_types, vec!["dns", "cn"]);
746        assert!(!Settings::default().allow_wildcards);
747    }
748}