[GH-ISSUE #2194] [RFC] DNSSEC validation: configuration syntax #914

Closed
opened 2026-03-16 00:52:12 +03:00 by kerem · 11 comments
Owner

Originally created by @japaric on GitHub (Apr 24, 2024).
Original GitHub issue: https://github.com/hickory-dns/hickory-dns/issues/2194

Motivation

Currently, the recursive resolver (AKA "recursor") component of the hickory-dns binary has DNSSEC validation disabled and cannot be enabled through its configuration options.

We want to be able to enable DNSSEC validation in the resolver.

Use cases

We want to support these use cases:

  • DNSSEC validation is disabled
  • DNSSEC validation is enabled

and when DNSSEC validation is enabled we have these sub- use cases:

  • use a static user-provided / externally-managed trust anchor
  • use a self-managed trust anchor, which is automatically updated given an initial trust anchor. see RFC5011

Prior art

unbound

To enable DNSSEC validation and use an externally-managed trust anchor, one has to set the server.trust-anchor-file setting to the path to the trust anchor file

server:
    trust-anchor-file: /etc/trust-key.key

To enable DNSSEC validation and use a self-managed (RFC5011) trust anchor, one has to set the server.auto-trust-anchor-file setting to the path to an initial trust anchor file

server:
    auto-trust-anchor-file: /etc/trust-key.key

An initial trust anchor file can be generated using the unbound-anchor tool.

More details about auto-trust-anchor-file and unbound-anchor can be found in https://nlnetlabs.nl/documentation/unbound/howto-anchor/

In both cases, the syntax of the trust anchor file is a newline-separated list of DNS records:

.	86400	IN	DNSKEY	257 3 7 (.. omitted base64-encoded data ..)
.	86400	IN	DNSKEY	256 3 7 (.. omitted base64-encoded data ..)

BIND (named)

named's configuration syntax is more complex. In the named.conf file, DNSSEC validation can be enabled / disabled using the options.dnssec-validation option:

options {
     dnssec-validation auto;
}

The trust anchor is configured in the file /etc/bind/bind.keys, which uses BIND-specific syntax instead of the DNS record syntax.

trust-anchors {
  . static-key 257 3 7 "(.. omitted base64-encoded data ..)";
  . static-key 256 3 7 "(.. omitted base64-encoded data ..)";
};

The static-key column indicates whether the key is externally managed (static-key) or managed according to RFC5011 (initial-key).

Current state

The recursive resolver component is enabled and configured using a [[zones]] section that looks like this:

[[zones]]
zone = "."
zone_type = "Hint"
stores = { type = "recursor", roots = "/tmp/root.hints" }

Where the stores entry corresponds to RecursiveConfig when type is recursor.

Proposal

Although hickory-dns configuration file is modeled after (BIND) named's configuration file, I would propose using a configuration syntax closer to unbound's. Namely, adding these two fields to RecursiveConfig

pub struct RecursiveConfig {
    pub roots: PathBuf,
    pub ns_cache_size: usize,
    pub record_cache_size: usize,
    /// Path to trust anchor file
    pub trust_anchor_file: Option<PathBuf>,
    /// Whether to manage the trust anchor file using RFC5011 or not (default: false)
    pub rfc5011: bool,
}
trust_anchor_file rfc5011 DNSSEC validation
None _ No
Some(_) false Yes, trust_anchor_file is static key
Some(_) true Yes, use RFC5011; trust_anchor_file is initial key

The syntax of the trust anchor file will be a newline-separated list of DNS records, either DS or DNSKEY records

This proposal can be implemented incrementally. We can add only trust_anchor_file first and only support static keys. We can then add rfc5011 when RFC5011 is implemented (if we want to implement it).

Alternatives

If we foresee supporting different trust anchor management strategies we could use a single enum field instead of an extra boolean field. Something like this:

pub struct RecursiveConfig {
    pub roots: PathBuf,
    pub ns_cache_size: usize,
    pub record_cache_size: usize,
    pub dnssec_validation: DnssecValidation,
}

#[non_exhaustive]
pub enum DnssecValidation {
    Disabled,
    // RFC5011
    InitialKey { file: PathBuf },
    // externally-managed key
    StaticKey { file: PathBuf },

    // .. other strategies ..
}
Originally created by @japaric on GitHub (Apr 24, 2024). Original GitHub issue: https://github.com/hickory-dns/hickory-dns/issues/2194 # Motivation Currently, the recursive resolver (AKA "recursor") component of the `hickory-dns` binary has DNSSEC validation [disabled](https://github.com/hickory-dns/hickory-dns/blob/6334a01430088ead8642cafaee592ec7bf49831f/crates/recursor/src/recursor.rs#L486) and cannot be enabled through its [configuration options](https://github.com/hickory-dns/hickory-dns/blob/6334a01430088ead8642cafaee592ec7bf49831f/crates/server/src/store/recursor/config.rs#L27). We want to be able to enable DNSSEC validation in the resolver. ## Use cases We want to support these use cases: - DNSSEC validation is disabled - DNSSEC validation is enabled and when DNSSEC validation is enabled we have these sub- use cases: - use a *static* user-provided / externally-managed trust anchor - use a *self-managed* trust anchor, which is automatically updated given an initial trust anchor. see [RFC5011](https://datatracker.ietf.org/doc/html/rfc5011) # Prior art ## `unbound` To enable DNSSEC validation and use an externally-managed trust anchor, one has to set the `server.trust-anchor-file` setting to the path to the trust anchor file ``` text server: trust-anchor-file: /etc/trust-key.key ``` To enable DNSSEC validation and use a self-managed (RFC5011) trust anchor, one has to set the `server.auto-trust-anchor-file` setting to the path to an initial trust anchor file ``` text server: auto-trust-anchor-file: /etc/trust-key.key ``` An initial trust anchor file can be generated using the `unbound-anchor` tool. More details about `auto-trust-anchor-file` and `unbound-anchor` can be found in <https://nlnetlabs.nl/documentation/unbound/howto-anchor/> In both cases, the syntax of the trust anchor file is a newline-separated list of DNS records: ``` text . 86400 IN DNSKEY 257 3 7 (.. omitted base64-encoded data ..) . 86400 IN DNSKEY 256 3 7 (.. omitted base64-encoded data ..) ``` ## BIND (`named`) `named`'s configuration syntax is more complex. In the `named.conf` file, DNSSEC validation can be enabled / disabled using the `options.dnssec-validation` option: ``` text options { dnssec-validation auto; } ``` The trust anchor is configured in the file `/etc/bind/bind.keys`, which uses BIND-specific syntax instead of the DNS record syntax. ``` text trust-anchors { . static-key 257 3 7 "(.. omitted base64-encoded data ..)"; . static-key 256 3 7 "(.. omitted base64-encoded data ..)"; }; ``` The `static-key` column indicates whether the key is externally managed (`static-key`) or managed according to RFC5011 (`initial-key`). # Current state The recursive resolver component is enabled and configured using a `[[zones]]` section that looks like this: ``` toml [[zones]] zone = "." zone_type = "Hint" stores = { type = "recursor", roots = "/tmp/root.hints" } ``` Where the `stores` entry corresponds to [`RecursiveConfig`](https://github.com/hickory-dns/hickory-dns/blob/6334a01430088ead8642cafaee592ec7bf49831f/crates/server/src/store/recursor/config.rs#L27) when `type` is `recursor`. # Proposal Although `hickory-dns` configuration file is modeled after (BIND) `named`'s configuration file, I would propose using a configuration syntax closer to `unbound`'s. Namely, adding these two fields to `RecursiveConfig` ``` rust pub struct RecursiveConfig { pub roots: PathBuf, pub ns_cache_size: usize, pub record_cache_size: usize, /// Path to trust anchor file pub trust_anchor_file: Option<PathBuf>, /// Whether to manage the trust anchor file using RFC5011 or not (default: false) pub rfc5011: bool, } ``` | `trust_anchor_file` | `rfc5011` | DNSSEC validation | |---------------------|-----------|--------------------------------------------------------| | `None` | `_` | No | | `Some(_)` | `false` | Yes, `trust_anchor_file` is *static* key | | `Some(_)` | `true` | Yes, use RFC5011; `trust_anchor_file` is *initial* key | The syntax of the trust anchor file will be a newline-separated list of DNS records, either DS or DNSKEY records This proposal can be implemented incrementally. We can add only `trust_anchor_file` first and only support static keys. We can then add `rfc5011` when RFC5011 is implemented (if we want to implement it). # Alternatives If we foresee supporting different trust anchor management strategies we could use a single `enum` field instead of an extra boolean field. Something like this: ``` rust pub struct RecursiveConfig { pub roots: PathBuf, pub ns_cache_size: usize, pub record_cache_size: usize, pub dnssec_validation: DnssecValidation, } #[non_exhaustive] pub enum DnssecValidation { Disabled, // RFC5011 InitialKey { file: PathBuf }, // externally-managed key StaticKey { file: PathBuf }, // .. other strategies .. } ```
Author
Owner

@djc commented on GitHub (Apr 24, 2024):

How does DnssecValidation get represented in TOML? I like that the enum approach is more explicit.

<!-- gh-comment-id:2074801288 --> @djc commented on GitHub (Apr 24, 2024): How does `DnssecValidation` get represented in TOML? I like that the enum approach is more explicit.
Author
Owner

@japaric commented on GitHub (Apr 24, 2024):

to use an enum with serde/toml we need to give pick a name for the tag field that will hold the enum variant name.

if we give the tag the name "mode" then the TOML would look like this

stores = {
    type = "recursor"
    roots = "/etc/root.hints",

    dnssec_validation = { 
        mode = "Disabled"
    }
    # Or
    dnssec_validation = { 
        mode = "InitialKey",
        file = "/etc/trusted.key"
    }
    # Or
    dnssec_validation = { 
        mode = "StaticKey",
        file = "/etc/trusted.key"
    }
}
<!-- gh-comment-id:2075309216 --> @japaric commented on GitHub (Apr 24, 2024): to use an `enum` with `serde`/`toml` we need to give pick a name for the tag field that will hold the enum variant name. if we give the tag the name "mode" then the TOML would look like this ``` toml stores = { type = "recursor" roots = "/etc/root.hints", dnssec_validation = { mode = "Disabled" } # Or dnssec_validation = { mode = "InitialKey", file = "/etc/trusted.key" } # Or dnssec_validation = { mode = "StaticKey", file = "/etc/trusted.key" } } ```
Author
Owner

@djc commented on GitHub (Apr 25, 2024):

That looks good to me.

<!-- gh-comment-id:2076572145 --> @djc commented on GitHub (Apr 25, 2024): That looks good to me.
Author
Owner

@japaric commented on GitHub (May 13, 2024):

given that we added a security_aware setting in #2196 we need to consider its interaction with dnssec_validation. to perform dnssec_validation, the recursor needs to be security_aware so we have a few options to represent these two settintgs:

one way is to keep the security_aware boolean next to the dnssec_validation enum we discussed, i.e.

struct RecursorConfig {
    #[serde(default = "false")]
    security_aware: bool,
    #[srede(default = "DnssecValidation::Disabled")]
    dnssec_validation: DnssecValidation,
}

#[non_exhaustive]
pub enum DnssecValidation {
    Disabled,
    // RFC5011
    InitialKey { file: PathBuf },
    // externally-managed key
    StaticKey { file: PathBuf },
    .// ..
}

in this case security_aware: false + DnssecValidation::{InitialKey,StaticKey} should be rejected with an error.

and possibly we'll want to accept this configuration

# security_aware has been omitted
dnssec_validation = { 
    mode = "InitialKey",
    file = "/etc/trusted.key"
}

where the omitted security_aware is understood to be true even though normally it defaults to true


the other set of options are to turn security_aware into an enum that wraps DnssecValidation.

enum SecurityAware {
    No,
    Yes(DnssecValidation),
}

#[non_exhaustive]
pub enum DnssecValidation {
    Disabled,
    // RFC5011
    InitialKey { file: PathBuf },
    // externally-managed key
    StaticKey { file: PathBuf },
    .// ..
}

or use a "flatter" representation with a single enum that captures the 4 cases.

enum Dnssec {
    SecurityUnaware, // default
    ValidationDisabled, // security-aware but no validation
    InitialKey { file: PathBuf },
    StaticKey { file: PathBuf },
}

both of these options don't allow "impossible" configurations (e.g. security_aware: false + DnssecValidation::InitialKey) that need to be linted for after the serde deserialization

<!-- gh-comment-id:2107046578 --> @japaric commented on GitHub (May 13, 2024): given that we added a `security_aware` setting in #2196 we need to consider its interaction with `dnssec_validation`. to perform `dnssec_validation`, the recursor needs to be `security_aware` so we have a few options to represent these two settintgs: one way is to keep the `security_aware` boolean next to the `dnssec_validation` enum we discussed, i.e. ```rust struct RecursorConfig { #[serde(default = "false")] security_aware: bool, #[srede(default = "DnssecValidation::Disabled")] dnssec_validation: DnssecValidation, } #[non_exhaustive] pub enum DnssecValidation { Disabled, // RFC5011 InitialKey { file: PathBuf }, // externally-managed key StaticKey { file: PathBuf }, .// .. } ``` in this case `security_aware: false` + `DnssecValidation::{InitialKey,StaticKey}` should be rejected with an error. and possibly we'll want to accept this configuration ``` # security_aware has been omitted dnssec_validation = { mode = "InitialKey", file = "/etc/trusted.key" } ``` where the omitted `security_aware` is understood to be `true` even though normally it defaults to `true` --- the other set of options are to turn `security_aware` into an enum that wraps `DnssecValidation`. ``` rust enum SecurityAware { No, Yes(DnssecValidation), } #[non_exhaustive] pub enum DnssecValidation { Disabled, // RFC5011 InitialKey { file: PathBuf }, // externally-managed key StaticKey { file: PathBuf }, .// .. } ``` or use a "flatter" representation with a single enum that captures the 4 cases. ``` rust enum Dnssec { SecurityUnaware, // default ValidationDisabled, // security-aware but no validation InitialKey { file: PathBuf }, StaticKey { file: PathBuf }, } ``` both of these options don't allow "impossible" configurations (e.g. `security_aware: false` + `DnssecValidation::InitialKey`) that need to be linted for after the serde deserialization
Author
Owner

@djc commented on GitHub (May 13, 2024):

I think I prefer the flatter representation.

Can you be explicit about what the difference is between security awareness and validation? In particular, being security aware without validating seems like a strange proposition.

<!-- gh-comment-id:2107312294 --> @djc commented on GitHub (May 13, 2024): I think I prefer the flatter representation. Can you be explicit about what the difference is between security awareness and validation? In particular, being security aware without validating seems like a strange proposition.
Author
Owner

@japaric commented on GitHub (May 13, 2024):

in practice, being a non-validating, security-aware, recursive resolver means that the DO (DNSSEC_OK) flag in client queries is respected, meaning that the response to the client will include DNSSEC data. This lets the client do the validation on its own, like delv does.

A validating (which implies also being security-aware) recursive resolver can be forced into behaving as a non-validating one by setting the CD (Checking Disabled) flag in the client's query.

Finally, a security-unaware, recursive resolver will ignore the DO flag in client queries and never report DNSSEC data back.

unbound and BIND, at least as packaged in Debian, are always security-aware but DNSSEC validation is opt-in.

<!-- gh-comment-id:2107625894 --> @japaric commented on GitHub (May 13, 2024): in practice, being a non-validating, security-aware, recursive resolver means that the DO (DNSSEC_OK) flag in client queries is respected, meaning that the response to the client will include DNSSEC data. This lets the client do the validation on its own, like `delv` does. A validating (which implies also being security-aware) recursive resolver can be forced into behaving as a non-validating one by setting the CD (Checking Disabled) flag in the client's query. Finally, a security-*un*aware, recursive resolver will ignore the DO flag in client queries and never report DNSSEC data back. `unbound` and BIND, at least as packaged in Debian, are always security-aware but DNSSEC validation is opt-in.
Author
Owner

@djc commented on GitHub (May 14, 2024):

Makes sense, thanks for the explanation! I think conveying similar context via comments in the configuration/implementation etc would be helpful.

<!-- gh-comment-id:2109845952 --> @djc commented on GitHub (May 14, 2024): Makes sense, thanks for the explanation! I think conveying similar context via comments in the configuration/implementation etc would be helpful.
Author
Owner

@bluejekyll commented on GitHub (May 18, 2024):

I like this suggestion. Right now we compile in the trust-anchors, from here: github.com/hickory-dns/hickory-dns@ede83dc7d6/crates/proto/src/rr/dnssec/trust_anchor.rs (L23-L24)

I think we might want to continue supporting that in addition to any future support for the RFC5011 standard. I don't think I see that as an option in your list...

<!-- gh-comment-id:2119018409 --> @bluejekyll commented on GitHub (May 18, 2024): I like this suggestion. Right now we compile in the trust-anchors, from here: https://github.com/hickory-dns/hickory-dns/blob/ede83dc7d6b3142eafe111bbebd41b73c9c3dbf0/crates/proto/src/rr/dnssec/trust_anchor.rs#L23-L24 I think we might want to continue supporting that in addition to any future support for the [RFC5011](https://datatracker.ietf.org/doc/html/rfc5011) standard. I don't think I see that as an option in your list...
Author
Owner

@japaric commented on GitHub (May 22, 2024):

I think we might want to continue supporting that in addition to any future support for the RFC5011 standard. I don't think I see that as an option in your list...

the InitialKey variant (of the Dnssec enum) corresponds to RFC5011. BIND uses the initial-key property (key?) in its settings to configure an initial key that's used to implement RFC5011.

I'm not sure if BIND or unbound hard-code a trust anchor in their source but the bind9 (Debian) package includes the file /etc/bind/bind.keys, which configures key 20326 (from 2017) as an "initial-key"; that is the trust anchor is provided in a separate file, not as part of the binary.

If we want to support a trust anchor built into the hickory-dns binary we could convert the file field in the InitialKey and StaticKey variants into an Option<PathBuf> where the None variant means "use the built-in trust anchor".

<!-- gh-comment-id:2124609741 --> @japaric commented on GitHub (May 22, 2024): > I think we might want to continue supporting that in addition to any future support for the [RFC5011](https://datatracker.ietf.org/doc/html/rfc5011) standard. I don't think I see that as an option in your list... the `InitialKey` variant (of the `Dnssec` enum) corresponds to RFC5011. BIND uses the `initial-key` property (key?) in its settings to configure an initial key that's used to implement RFC5011. I'm not sure if BIND or unbound hard-code a trust anchor in their source but the `bind9` (Debian) package includes the file `/etc/bind/bind.keys`, which configures key 20326 (from 2017) as an "initial-key"; that is the trust anchor is provided in a separate file, not as part of the binary. If we want to support a trust anchor built into the `hickory-dns` binary we could convert the `file` field in the `InitialKey` and `StaticKey` variants into an `Option<PathBuf>` where the `None` variant means "use the built-in trust anchor".
Author
Owner

@bluejekyll commented on GitHub (May 22, 2024):

I like the built-in keys as it makes the key management simpler for initializing those roots.

<!-- gh-comment-id:2125780064 --> @bluejekyll commented on GitHub (May 22, 2024): I like the built-in keys as it makes the key management simpler for initializing those roots.
Author
Owner

@japaric commented on GitHub (Jun 18, 2024):

update here: as I'm implementing this RFC I realized hickory-dns is using the basic-toml crate which does not support serializing nor deserializing enum variants that have fields like the ones (Dnssec) we want to use here. the toml crate does support such enums so I'll use the toml crate in an initial implementation. we can discuss if we want to switch to toml or change the layout of Dnssec

<!-- gh-comment-id:2176497799 --> @japaric commented on GitHub (Jun 18, 2024): update here: as I'm implementing this RFC I realized `hickory-dns` is using the `basic-toml` crate which does not support serializing nor deserializing `enum` variants that have fields like the ones (`Dnssec`) we want to use here. the `toml` crate does support such `enum`s so I'll use the `toml` crate in an initial implementation. we can discuss if we want to switch to `toml` or change the layout of `Dnssec`
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
starred/hickory-dns#914
No description provided.