// Package config loads jarld's configuration file. package config import ( "bytes" "fmt" "net" "net/netip" "net/url" "os" "path/filepath" "regexp" "strings" "time" "go.yaml.in/yaml/v3" ) // DefaultPath is where jarld looks for its configuration. const DefaultPath = "/etc/jarl/jarl.yaml" // Config is the daemon configuration. type Config struct { Listen []string `yaml:"listen"` TLS TLS `yaml:"tls"` TrustedProxies []string `yaml:"trusted_proxies"` // Socket is a local unix socket for the API, where root and jarld's // own user need no token ("" for none). Socket string `yaml:"socket"` Database string `yaml:"database"` StagingDir string `yaml:"staging_dir"` Log Log `yaml:"log"` Session Session `yaml:"session"` TOTP TOTP `yaml:"totp"` BIND BIND `yaml:"bind"` Audit Audit `yaml:"audit"` Backup Backup `yaml:"backup"` Stats Stats `yaml:"stats"` ACME ACME `yaml:"acme"` Porkbun Porkbun `yaml:"porkbun"` Blocking Blocking `yaml:"blocking"` Metrics Metrics `yaml:"metrics"` // Certificates configures TLS certificates Jarl manages // (docs/certificates.md); the ACME account is under acme. Certificates Certificates `yaml:"certificates"` } // Certificates configures the certificates Jarl manages. type Certificates struct { // Dir is where certificates are written for named (DNS over TLS and // HTTPS): one directory per certificate, readable by bind.file_group. // "" is /tls. Dir string `yaml:"dir"` } // Metrics configures the Prometheus endpoint /metrics (docs/metrics.md). // A scrape is allowed from an address in Allow, or with Token as a bearer // token; root and jarld's own user may also scrape on the local socket. type Metrics struct { Enabled bool `yaml:"enabled"` // Listen are extra addresses that serve only /metrics, over plain // HTTP. /metrics is always served on the main listeners too. Listen []string `yaml:"listen"` // Allow are the addresses or prefixes that may scrape without a token. Allow []string `yaml:"allow"` // Token, when set, lets any address that sends it as // "Authorization: Bearer TOKEN" scrape. Token string `yaml:"token"` } func (m *Metrics) check() error { if !m.Enabled { return nil } for _, a := range m.Listen { if _, port, err := net.SplitHostPort(a); err != nil || port == "" { return fmt.Errorf("listen %q must be ADDRESS:PORT, e.g. 0.0.0.0:9450", a) } } for _, p := range m.Allow { if _, err := ParsePrefix(p); err != nil { return fmt.Errorf("allow: %v", err) } } if m.Token != "" && (len(m.Token) < 16 || strings.ContainsAny(m.Token, " \t\r\n")) { return fmt.Errorf("token must be at least 16 characters, without spaces (e.g. from 'openssl rand -hex 24')") } if len(m.Allow) == 0 && m.Token == "" { return fmt.Errorf("set allow, token or both, or nobody can scrape") } return nil } // AllowPrefixes returns the parsed allow list. func (m *Metrics) AllowPrefixes() []netip.Prefix { var out []netip.Prefix for _, p := range m.Allow { if pf, err := ParsePrefix(p); err == nil { out = append(out, pf.Masked()) } } return out } // Blocking configures blocklist downloads (docs/blocking.md). The lists // and rules themselves are configuration in the database. type Blocking struct { // Refresh is how often each list is checked for changes; a list is // only downloaded again when it changed. Refresh Duration `yaml:"refresh"` MaxDownloadMB int `yaml:"max_download_mb"` MaxEntries int `yaml:"max_entries"` } func (b *Blocking) check() error { if b.Refresh < Duration(time.Hour) { return fmt.Errorf("refresh must be at least 1h (list hosts ask not to be polled more often)") } if b.MaxDownloadMB < 1 || b.MaxEntries < 1 { return fmt.Errorf("max_download_mb and max_entries must be at least 1") } return nil } // Porkbun lets Jarl manage domains' name servers, glue records and DNSSEC // DS records at the registrar Porkbun (docs/dnssec.md#porkbun). The key // lives only here, in a file root and jarld can read, and never in the // database (so not in backups either). type Porkbun struct { APIKey string `yaml:"api_key"` SecretAPIKey string `yaml:"secret_api_key"` // Domains limits which domains Jarl may manage at Porkbun (default: // every Jarl zone the key can reach). Domains []string `yaml:"domains"` // APIURL is the API's base URL (for tests). APIURL string `yaml:"api_url"` } // Enabled reports whether a Porkbun key is configured. func (p *Porkbun) Enabled() bool { return p.APIKey != "" } // Allows reports whether Jarl may manage domain (no trailing dot) at // Porkbun. func (p *Porkbun) Allows(domain string) bool { if !p.Enabled() { return false } if len(p.Domains) == 0 { return true } for _, d := range p.Domains { if strings.EqualFold(strings.TrimSuffix(d, "."), domain) { return true } } return false } func (p *Porkbun) check() error { if (p.APIKey == "") != (p.SecretAPIKey == "") { return fmt.Errorf("set both api_key and secret_api_key (or neither)") } if p.APIURL != "" { u, err := url.Parse(p.APIURL) if err != nil || (u.Scheme != "https" && !(u.Scheme == "http" && isLoopbackHost(u.Hostname()))) { return fmt.Errorf("api_url must be an https:// URL") } } return nil } func isLoopbackHost(h string) bool { if h == "localhost" { return true } a, err := netip.ParseAddr(h) return err == nil && a.IsLoopback() } // Backup configures 'jarld backup' run without a file name, which is what // the jarl-backup.timer systemd timer runs (daily by default; the schedule // is the timer's OnCalendar=). Each run writes a new backup into Dir and // then deletes the ones Retention and KeepAtLeast no longer want. type Backup struct { Dir string `yaml:"dir"` // Retention deletes backups older than this (0 keeps them all)... Retention Duration `yaml:"retention"` // ...but the newest KeepAtLeast are always kept, however old. KeepAtLeast int `yaml:"keep_at_least"` // Stats also backs up the query statistics database. Stats bool `yaml:"stats"` // WarnAfter makes the dashboard warn when the newest backup is older // than this (0: never). Keep it above the timer's interval. WarnAfter Duration `yaml:"warn_after"` } // ACME configures the ACME helpers and the account Jarl uses to obtain // certificates itself. type ACME struct { HTTPReq HTTPReq `yaml:"httpreq"` Account ACMEAccount `yaml:"account"` // Resolvers are the recursive resolvers used to see a domain as the // CA does: its zone, its public name servers and any CNAME on // _acme-challenge. Public resolvers by default, because this server's // own answers may come from an internal view. Resolvers []string `yaml:"resolvers"` // CABundle is a PEM file of extra CA certificates to trust when // talking to the ACME server (a private CA such as step-ca). CABundle string `yaml:"ca_bundle"` // CheckPropagation waits until every name server of the zone serves // a challenge before telling the CA to look. CheckPropagation bool `yaml:"check_propagation"` } // ACMEAccount is the ACME account certificates are obtained with. type ACMEAccount struct { // Directory is letsencrypt, letsencrypt-staging or an ACME directory URL. Directory string `yaml:"directory"` Email string `yaml:"email"` // AgreeTOS says you accept the CA's terms of service. Jarl doesn't // register an account without it. AgreeTOS bool `yaml:"agree_tos"` // EABKeyID and EABHMAC are External Account Binding credentials, for // CAs that need them (ZeroSSL, Google). EABKeyID string `yaml:"eab_kid"` EABHMAC string `yaml:"eab_hmac"` } // Directory URLs for the names acme.account.directory accepts. const ( LetsEncrypt = "https://acme-v02.api.letsencrypt.org/directory" LetsEncryptStaging = "https://acme-staging-v02.api.letsencrypt.org/directory" ) // DirectoryURL is the account's ACME directory URL. func (a ACMEAccount) DirectoryURL() string { switch a.Directory { case "", "letsencrypt": return LetsEncrypt case "letsencrypt-staging": return LetsEncryptStaging } return a.Directory } func (a *ACME) check() error { if d := a.Account.Directory; d != "" && d != "letsencrypt" && d != "letsencrypt-staging" { u, err := url.Parse(d) if err != nil || u.Scheme != "https" || u.Host == "" { return fmt.Errorf("account.directory must be letsencrypt, letsencrypt-staging or an https:// URL") } } if a.Account.Email != "" && !strings.Contains(a.Account.Email, "@") { return fmt.Errorf("account.email %q isn't an email address", a.Account.Email) } if (a.Account.EABKeyID == "") != (a.Account.EABHMAC == "") { return fmt.Errorf("account: set both eab_kid and eab_hmac, or neither") } for _, r := range a.Resolvers { if _, _, err := net.SplitHostPort(r); err != nil { return fmt.Errorf("resolvers: %q must be address:port, e.g. 1.1.1.1:53 or [2606:4700:4700::1111]:53", r) } } if a.CABundle != "" && !filepath.IsAbs(a.CABundle) { return fmt.Errorf("ca_bundle must be an absolute path") } return nil } // CertDir is certificates.dir, or /tls. func (c *Config) CertDir() string { if c.Certificates.Dir != "" { return c.Certificates.Dir } return filepath.Join(c.BIND.ConfigDir, "tls") } // HTTPReq configures the endpoint for lego's httpreq DNS provider // (docs/acme.md). lego sends an API token as an HTTP Basic password, so // by default the endpoint only answers requests that came over HTTPS. type HTTPReq struct { Enabled bool `yaml:"enabled"` // RequireHTTPS refuses requests that didn't arrive over HTTPS: jarld's // own TLS, or a trusted proxy that says so in X-Forwarded-Proto or // Forwarded. RequireHTTPS bool `yaml:"require_https"` // AllowLoopbackHTTP also accepts plain HTTP from 127.0.0.1 and ::1 // (lego on the DNS server itself), unless that address is a trusted // proxy. AllowLoopbackHTTP bool `yaml:"allow_loopback_http"` } // TLS configures HTTPS. type TLS struct { CertFile string `yaml:"cert_file"` KeyFile string `yaml:"key_file"` } // Log configures logging. type Log struct { Level string `yaml:"level"` // debug, info, warn, error Format string `yaml:"format"` // text, json } // Session configures web sessions. type Session struct { Lifetime Duration `yaml:"lifetime"` IdleTimeout Duration `yaml:"idle_timeout"` CookieSecure string `yaml:"cookie_secure"` // auto, always, never } // BIND says where BIND's files and tools are. // TOTP configures two-factor login to the web UI (docs/permissions.md). type TOTP struct { // Require is who must use it: none (each user decides), admins or // all (every user who logs in with a password). Require string `yaml:"require"` // Issuer is the name authenticator apps show for the account ("" is // "Jarl "). Issuer string `yaml:"issuer"` } // IssuerName is Issuer, or "Jarl ". func (t TOTP) IssuerName() string { if t.Issuer != "" { return t.Issuer } h, _ := os.Hostname() if h, _, _ = strings.Cut(h, "."); h == "" { return "Jarl" } return "Jarl " + h } type BIND struct { MainConfig string `yaml:"main_config"` ConfigDir string `yaml:"config_dir"` ZoneDir string `yaml:"zone_dir"` CacheDir string `yaml:"cache_dir"` FileGroup string `yaml:"file_group"` // NamedUser is the user named runs as. 'jarld restore' gives it the // DNSSEC keys it puts back. NamedUser string `yaml:"named_user"` NamedCheckConf string `yaml:"named_checkconf"` NamedCheckZone string `yaml:"named_checkzone"` RNDC string `yaml:"rndc"` RNDCArgs []string `yaml:"rndc_args"` ReloadTimeout Duration `yaml:"reload_timeout"` NoReload bool `yaml:"no_reload"` } // Audit configures the audit log. type Audit struct { // RetentionDays deletes older audit events (0 keeps everything). RetentionDays int `yaml:"retention_days"` } // Stats configures query statistics. When enabled, Jarl makes named publish // its counters on a local statistics channel and log every response to a // file, and jarld aggregates both into a separate database. type Stats struct { Enabled bool `yaml:"enabled"` // QueryLog enables per-query data (top names and clients, per-view and // per-zone counts) from named's response log. Without it only named's // own counters are collected. QueryLog bool `yaml:"query_log"` // RateLimitLog sends rate limiting's log lines (named's query-errors // category) to the response log too, so the Exposure page and the // statistics can show what was, or would be, limited. RateLimitLog bool `yaml:"rate_limit_log"` // Database is the statistics database, kept apart from Jarl's // configuration database. Database string `yaml:"database"` // StatisticsChannel is the address named serves its counters on // (only reachable from this host). StatisticsChannel string `yaml:"statistics_channel"` // LogFile is where named logs responses. named's AppArmor profile // allows writing under /var/log/named. LogFile string `yaml:"log_file"` LogSize string `yaml:"log_size"` LogVersions int `yaml:"log_versions"` Retention StatsRetention `yaml:"retention"` // TopN is how many names and clients are kept per hour and per day. TopN int `yaml:"top_n"` // AnonymizeClients stores client networks (IPv4 /24, IPv6 /48) // instead of addresses. AnonymizeClients bool `yaml:"anonymize_clients"` // ExcludeClients are addresses or prefixes whose queries are not // counted per name or client (e.g. monitoring probes). ExcludeClients []string `yaml:"exclude_clients"` } // StatsRetention says how long each resolution of the statistics is kept. type StatsRetention struct { Minute Duration `yaml:"minute"` Hour Duration `yaml:"hour"` Day Duration `yaml:"day"` } // Duration is a time.Duration written as "15s" or "12h" in YAML. Days // ("30d") are accepted too. type Duration time.Duration // UnmarshalYAML implements yaml.Unmarshaler. func (d *Duration) UnmarshalYAML(n *yaml.Node) error { var s string if err := n.Decode(&s); err != nil { return err } v, err := ParseDuration(s) if err != nil { return err } *d = Duration(v) return nil } // MarshalYAML implements yaml.Marshaler. func (d Duration) MarshalYAML() (any, error) { return time.Duration(d).String(), nil } // ParseDuration parses a Go duration, also accepting a "d" suffix. func ParseDuration(s string) (time.Duration, error) { s = strings.TrimSpace(s) if n, ok := strings.CutSuffix(s, "d"); ok { var days int if _, err := fmt.Sscanf(n, "%d", &days); err != nil || fmt.Sprint(days) != n { return 0, fmt.Errorf("invalid duration %q", s) } return time.Duration(days) * 24 * time.Hour, nil } return time.ParseDuration(s) } // Default returns the defaults for a Debian/Ubuntu BIND 9.20 server. func Default() *Config { return &Config{ Listen: []string{"127.0.0.1:8053"}, Socket: "/run/jarl/jarl.sock", Database: "/var/lib/jarl/jarl.db", StagingDir: "/var/lib/jarl/staging", Log: Log{Level: "info", Format: "text"}, Session: Session{Lifetime: Duration(12 * time.Hour), IdleTimeout: Duration(2 * time.Hour), CookieSecure: "auto"}, TOTP: TOTP{Require: "none"}, BIND: BIND{ MainConfig: "/etc/bind/named.conf", ConfigDir: "/etc/bind/jarl", ZoneDir: "/var/lib/bind/jarl", CacheDir: "/var/cache/bind/jarl", FileGroup: "bind", NamedUser: "bind", NamedCheckConf: "/usr/bin/named-checkconf", NamedCheckZone: "/usr/bin/named-checkzone", RNDC: "/usr/sbin/rndc", ReloadTimeout: Duration(15 * time.Second), }, Stats: Stats{ QueryLog: true, RateLimitLog: true, Database: "/var/lib/jarl/stats.db", StatisticsChannel: "127.0.0.1:8058", LogFile: "/var/log/named/jarl-responses.log", LogSize: "20m", LogVersions: 2, Retention: StatsRetention{Minute: Duration(7 * 24 * time.Hour), Hour: Duration(90 * 24 * time.Hour), Day: Duration(730 * 24 * time.Hour)}, TopN: 100, }, ACME: ACME{HTTPReq: HTTPReq{RequireHTTPS: true}, Account: ACMEAccount{Directory: "letsencrypt"}, Resolvers: []string{"1.1.1.1:53", "9.9.9.9:53", "8.8.8.8:53"}, CheckPropagation: true}, Blocking: Blocking{Refresh: Duration(24 * time.Hour), MaxDownloadMB: 64, MaxEntries: 1_000_000}, Metrics: Metrics{Allow: []string{"127.0.0.1", "::1"}}, Backup: Backup{Dir: "/var/backups/jarl", Retention: Duration(30 * 24 * time.Hour), KeepAtLeast: 7, WarnAfter: Duration(2 * 24 * time.Hour)}, } } // Load reads the configuration file at path over the defaults. A missing // file is not an error when allowMissing is set. func Load(path string, allowMissing bool) (*Config, error) { c := Default() b, err := os.ReadFile(path) if err != nil { if os.IsNotExist(err) && allowMissing { return c, c.check() } return nil, err } dec := yaml.NewDecoder(bytes.NewReader(b)) dec.KnownFields(true) if err := dec.Decode(c); err != nil && err.Error() != "EOF" { return nil, fmt.Errorf("%s: %w", path, err) } return c, c.check() } func (c *Config) check() error { if len(c.Listen) == 0 { return fmt.Errorf("listen: at least one address is needed") } if c.Socket != "" && !filepath.IsAbs(c.Socket) { return fmt.Errorf("socket must be an absolute path (or \"\" for none)") } if len(c.Socket) > 107 { return fmt.Errorf("socket: the path is %d bytes long; unix sockets allow at most 107", len(c.Socket)) } if (c.TLS.CertFile == "") != (c.TLS.KeyFile == "") { return fmt.Errorf("tls: set both cert_file and key_file") } for _, p := range c.TrustedProxies { if _, err := ParsePrefix(p); err != nil { return fmt.Errorf("trusted_proxies: %v", err) } } for name, p := range map[string]string{"database": c.Database, "bind.config_dir": c.BIND.ConfigDir, "bind.zone_dir": c.BIND.ZoneDir, "bind.cache_dir": c.BIND.CacheDir} { if p == "" || !filepath.IsAbs(p) { return fmt.Errorf("%s must be an absolute path", name) } } switch c.Session.CookieSecure { case "auto", "always", "never": default: return fmt.Errorf("session.cookie_secure must be auto, always or never") } switch c.TOTP.Require { case "none", "admins", "all": default: return fmt.Errorf("totp.require must be none, admins or all") } if len(c.TOTP.Issuer) > 64 || strings.ContainsAny(c.TOTP.Issuer, ":") { return fmt.Errorf("totp.issuer must be at most 64 characters, without ':'") } if err := c.Stats.check(); err != nil { return fmt.Errorf("stats: %w", err) } if err := c.Backup.check(); err != nil { return fmt.Errorf("backup: %w", err) } if err := c.Porkbun.check(); err != nil { return fmt.Errorf("porkbun: %w", err) } if err := c.Blocking.check(); err != nil { return fmt.Errorf("blocking: %w", err) } if err := c.Metrics.check(); err != nil { return fmt.Errorf("metrics: %w", err) } if err := c.ACME.check(); err != nil { return fmt.Errorf("acme: %w", err) } if d := c.Certificates.Dir; d != "" && !filepath.IsAbs(d) { return fmt.Errorf("certificates.dir must be an absolute path") } switch c.Log.Format { case "text", "json": default: return fmt.Errorf("log.format must be text or json") } return nil } func (b *Backup) check() error { if b.Dir == "" || !filepath.IsAbs(b.Dir) { return fmt.Errorf("dir must be an absolute path") } if b.Retention < 0 || (b.Retention > 0 && b.Retention < Duration(24*time.Hour)) { return fmt.Errorf("retention must be at least 1d (or 0 to keep every backup)") } if b.KeepAtLeast < 1 { return fmt.Errorf("keep_at_least must be at least 1") } if b.WarnAfter < 0 || (b.WarnAfter > 0 && b.WarnAfter < Duration(time.Hour)) { return fmt.Errorf("warn_after must be at least 1h (or 0 for no warning)") } return nil } func (s *Stats) check() error { if !s.Enabled { return nil } if s.Database == "" || !filepath.IsAbs(s.Database) { return fmt.Errorf("database must be an absolute path") } ap, err := netip.ParseAddrPort(s.StatisticsChannel) if err != nil { return fmt.Errorf("statistics_channel %q must be ADDRESS:PORT, e.g. 127.0.0.1:8058", s.StatisticsChannel) } if !ap.Addr().IsLoopback() { return fmt.Errorf("statistics_channel must be a loopback address (named's counters must not be reachable from the network)") } if s.QueryLog { if !filepath.IsAbs(s.LogFile) { return fmt.Errorf("log_file must be an absolute path") } if strings.ContainsAny(s.LogFile, "\"\n") { return fmt.Errorf("log_file contains invalid characters") } if !logSizeRe.MatchString(s.LogSize) { return fmt.Errorf("log_size %q must be a size such as 20m or 1g", s.LogSize) } if s.LogVersions < 1 || s.LogVersions > 100 { return fmt.Errorf("log_versions must be between 1 and 100") } } r := s.Retention if r.Minute < Duration(time.Hour) || r.Hour < Duration(24*time.Hour) || r.Day < Duration(24*time.Hour) { return fmt.Errorf("retention must keep minutes for at least 1h, hours and days for at least 1d") } if s.TopN < 10 || s.TopN > 10000 { return fmt.Errorf("top_n must be between 10 and 10000") } for _, p := range s.ExcludeClients { if _, err := ParsePrefix(p); err != nil { return fmt.Errorf("exclude_clients: %v", err) } } return nil } var logSizeRe = regexp.MustCompile(`^[0-9]+[kKmMgG]?$`) // ParsePrefix parses an address or prefix. func ParsePrefix(s string) (netip.Prefix, error) { if strings.Contains(s, "/") { return netip.ParsePrefix(s) } a, err := netip.ParseAddr(s) if err != nil { return netip.Prefix{}, err } return netip.PrefixFrom(a, a.BitLen()), nil } // Proxies returns the trusted proxy prefixes. func (c *Config) Proxies() []netip.Prefix { var out []netip.Prefix for _, p := range c.TrustedProxies { if pf, err := ParsePrefix(p); err == nil { out = append(out, pf.Masked()) } } return out } // Example is a commented example configuration. const Example = `# Jarl daemon configuration (/etc/jarl/jarl.yaml). # Every setting is optional; the values shown are the defaults for # Debian/Ubuntu with BIND 9.20. # Addresses the web UI and API listen on. Keep this off the internet: # bind to localhost or a management network, and use TLS (below) or a # TLS-terminating reverse proxy. listen: - "127.0.0.1:8053" # HTTPS from a certificate file you renew yourself. The certificate is # reread when the file changes (or on SIGHUP). Easier: let Jarl obtain one # ('jarl cert create ... --use web', docs/certificates.md), which then # takes over from this. tls: cert_file: "" key_file: "" # A local unix socket for the API and the jarl CLI. Requests on it from # root or jarld's own user need no token and have administrator rights # ('sudo jarl status' works with nothing configured); anyone else must send # a token. systemd creates /run/jarl. "" turns the socket off. socket: /run/jarl/jarl.sock # Reverse proxies whose X-Forwarded-For / X-Forwarded-Proto headers are # trusted (addresses or prefixes). trusted_proxies: [] database: /var/lib/jarl/jarl.db staging_dir: /var/lib/jarl/staging log: level: info # debug, info, warn or error format: text # text or json (both go to the journal under systemd) session: lifetime: 12h # web sessions end after this long... idle_timeout: 2h # ...or after this long without activity cookie_secure: auto # auto, always or never # Two-factor login to the web UI with an authenticator app (TOTP). Each # user can turn it on under Account. require makes it compulsory: none, # admins, or all (every user with a password). A user who must use it and # hasn't set it up can only set it up after logging in. API tokens and # the local socket are not affected. See docs/permissions.md. totp: require: none issuer: "" # the name in the app; "" is "Jarl " bind: # The file named loads. 'jarld adopt' makes it include Jarl's config. main_config: /etc/bind/named.conf # Where Jarl writes named.conf/keys.conf, zone files, and where named # keeps files of secondary zones. named (AppArmor) may read /etc/bind # and write /var/lib/bind and /var/cache/bind. config_dir: /etc/bind/jarl zone_dir: /var/lib/bind/jarl cache_dir: /var/cache/bind/jarl # Group of generated files; named must be able to read them. file_group: bind # The user named runs as ('jarld restore' gives it the DNSSEC keys back). named_user: bind named_checkconf: /usr/bin/named-checkconf named_checkzone: /usr/bin/named-checkzone rndc: /usr/sbin/rndc # Extra rndc arguments, e.g. ["-k", "/etc/bind/rndc.key", "-s", "127.0.0.1"]. rndc_args: [] # How long to wait for named to load changed zones before rolling back. reload_timeout: 15s # Write files but never run rndc (no named on this host). no_reload: false audit: # Delete audit events older than this many days (0 keeps them forever). retention_days: 0 # Database backups (docs/operations.md#backups). The jarl-backup.timer # systemd timer runs 'jarld backup' daily; change when with # 'systemctl edit jarl-backup.timer'. Each run writes # DIR/jarl-.db, checks it, then deletes backups older than # 'retention', always keeping the newest 'keep_at_least'. Only files named # like that are ever deleted. Each backup also holds the DNSSEC keys of # signed zones (private keys included), so keep the directory private. # Restore with 'jarld restore'. backup: dir: /var/backups/jarl # the timer's sandbox may write only here (see the docs to move it) retention: 30d # 0 keeps every backup keep_at_least: 7 stats: false # also back up the statistics database warn_after: 2d # dashboard warning when the newest backup is older (0: never) # Query statistics (docs/stats.md). Off by default. When enabled, Jarl makes # named publish its counters on a local statistics channel and log every # response, and jarld aggregates both into a separate database. Changing # this section takes effect when jarld restarts (it rewrites named.conf). stats: enabled: false # Per-query data (top names and clients, per view and zone). false # collects only named's own counters, with no logging overhead. query_log: true # With a rate-limit set, also log what rate limiting limits (or would, # in log-only mode): named's query-errors category, which Jarl then adds # to its log channel (and keeps in syslog). The Exposure page shows it. rate_limit_log: true database: /var/lib/jarl/stats.db statistics_channel: 127.0.0.1:8058 log_file: /var/log/named/jarl-responses.log log_size: 20m log_versions: 2 retention: minute: 7d # per-minute counters hour: 90d # hourly counters and top lists day: 730d # daily counters and top lists top_n: 100 # names and clients kept per hour and per day anonymize_clients: false # store 10.10.0.0/24 instead of 10.10.0.37 exclude_clients: [] # e.g. [127.0.0.1] to ignore local monitoring # An endpoint for lego's "httpreq" DNS provider (docs/acme.md), so lego and # Traefik can publish DNS-01 challenges without the jarl CLI. Off by # default. lego sends an API token as an HTTP Basic password, so by default # requests are only accepted over HTTPS: jarld's own tls (above), or a # reverse proxy in trusted_proxies that sends "X-Forwarded-Proto: https" # (or "Forwarded: proto=https"). Nothing else in Jarl depends on this. acme: httpreq: enabled: false require_https: true # Also accept plain HTTP from 127.0.0.1 and ::1, for lego running on # this server. Leave false if a reverse proxy runs on this server but # isn't in trusted_proxies: its requests would look local. allow_loopback_http: false # The account Jarl obtains certificates with (docs/certificates.md). # Nothing is registered until you create a certificate. Read the CA's # terms (https://letsencrypt.org/repository/) and set agree_tos: true. # Try letsencrypt-staging first: its certificates aren't trusted, but # its rate limits are generous. account: directory: letsencrypt # letsencrypt, letsencrypt-staging or a directory URL email: "" # optional agree_tos: false eab_kid: "" # External Account Binding, only for CAs that need it eab_hmac: "" # Resolvers to see your domains as the CA does (their public name # servers, a CNAME on _acme-challenge). Not this server: it may answer # from an internal view. resolvers: ["1.1.1.1:53", "9.9.9.9:53", "8.8.8.8:53"] ca_bundle: "" # extra CA certificates for a private ACME server # Wait until every name server of the zone serves a challenge before # telling the CA to look (they are asked directly, on port 53). check_propagation: true # Where certificates with the "dns" use are written for named, one # directory per certificate (fullchain.pem, privkey.pem), readable by # bind.file_group. Default: /tls. certificates: dir: "" # Porkbun, as registrar and as DNS host. With an API key, Jarl can: # - show and set a domain's name servers, glue records and DNSSEC DS # records there, after checking that the change won't break the domain # (docs/dnssec.md#porkbun); # - publish the DNS-01 challenges of certificates Jarl obtains while # Porkbun still serves the domain's DNS (docs/certificates.md): it adds # and removes TXT records at _acme-challenge names only. # Off without a key. Create the key at porkbun.com (Account -> API Access), # switch on "API Access" for the domain, and limit the key to that domain # if you can. The key can move your domain elsewhere: it lives only in this # file (keep it readable by root and jarld only: chown root:jarl, chmod # 640), never in Jarl's database or backups. porkbun: api_key: "" # pk1_... secret_api_key: "" # sk1_... domains: [] # only these domains (default: any Jarl zone the key reaches) # Downloading blocklists (docs/blocking.md). The lists and rules are set up # in the web UI or with 'jarl blocklist' and 'jarl block'; this only sets # how jarld downloads them. blocking: refresh: 24h # check each list for changes this often (at least 1h) max_download_mb: 64 # refuse larger downloads max_entries: 1000000 # refuse lists with more domains # Prometheus metrics at /metrics (docs/metrics.md): named's counters and # Jarl's own (blocking, zones, views), plus health gauges to alert on # (named up, validation problems, blocklist errors, backup age, DNSSEC # steps, token expiry). Off by default. /metrics is served on the # addresses in 'listen' at the top of this file; a scrape is allowed from # an address in 'allow' below, or with the token. Query counters need # stats.enabled. metrics: enabled: false # Extra addresses that serve only /metrics, over plain HTTP, e.g. # ["0.0.0.0:9450"] for a Prometheus server elsewhere on the network. listen: [] # Addresses or prefixes that may scrape without a token. allow: ["127.0.0.1", "::1"] # A bearer token (at least 16 characters) that lets any address scrape: # in Prometheus, 'authorization: { credentials: TOKEN }'. Keep this file # readable by root and jarld only when you set it. token: "" `