mirror of
https://github.com/juanfont/headscale.git
synced 2026-10-23 23:06:49 +09:00
types: add dns.challenge and dns.certificates config
dns.challenge.provider builds a libdns provider at load, so configtest catches typos. dns.certificates gates node certs; zone defaults to base_domain. Updates #2137
This commit is contained in:
1 parent
6cb950cd9e
commit
0efe4b7798
3 files changed
+399
-5
No files matched your search
@@ -358,6 +358,30 @@ dns:
|
||||
# Headscale processes this file on each change.
|
||||
# extra_records_path: /var/lib/headscale/extra-records.json
|
||||
|
||||
# Let nodes get TLS certificates for their MagicDNS name with
|
||||
# `tailscale cert`. Requires dns.challenge.provider.
|
||||
certificates:
|
||||
enabled: false
|
||||
|
||||
# DNS zone the provider writes to. Defaults to base_domain.
|
||||
# zone: example.com
|
||||
|
||||
# DNS provider for ACME DNS-01 challenges, used by dns.certificates.
|
||||
challenge:
|
||||
# How long to wait for a record to show up on every authoritative
|
||||
# nameserver. 0 disables the check.
|
||||
propagation_timeout: 2m
|
||||
|
||||
# Resolvers used to find the zone's nameservers.
|
||||
# Defaults to 1.1.1.1 and 8.8.8.8.
|
||||
# resolvers: []
|
||||
|
||||
# cloudflare, gigahost, hetzner, powerdns or rfc2136. The other keys are
|
||||
# settings of that provider; add _path to a key to read it from a file.
|
||||
# provider:
|
||||
# name: cloudflare
|
||||
# api_token_path: /run/credentials/headscale.service/cloudflare-token
|
||||
|
||||
# Unix socket used for the CLI to connect without authentication
|
||||
# Note: for production you will want to set this to something like:
|
||||
unix_socket: /var/run/headscale/headscale.sock
|
||||
|
||||
+167
-4
@@ -1,17 +1,21 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"net"
|
||||
"net/netip"
|
||||
"net/url"
|
||||
"os"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/coreos/go-oidc/v3/oidc"
|
||||
hsdns "github.com/juanfont/headscale/hscontrol/dns"
|
||||
"github.com/juanfont/headscale/hscontrol/util"
|
||||
"github.com/prometheus/common/model"
|
||||
"github.com/rs/zerolog"
|
||||
@@ -21,6 +25,7 @@ import (
|
||||
"tailscale.com/net/tsaddr"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/dnstype"
|
||||
"tailscale.com/util/dnsname"
|
||||
"tailscale.com/util/set"
|
||||
)
|
||||
|
||||
@@ -29,6 +34,8 @@ const (
|
||||
PKCEMethodS256 string = "S256"
|
||||
|
||||
defaultNodeStoreBatchSize = 100
|
||||
|
||||
dnsDocsURL = "https://headscale.net/stable/ref/dns/"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -44,6 +51,12 @@ var (
|
||||
ErrInvalidAllocationStrategy = errors.New("invalid prefix allocation strategy")
|
||||
)
|
||||
|
||||
// defaultChallengeResolvers find a zone's nameservers when
|
||||
// dns.challenge.resolvers is empty. The system resolver is not used: on a
|
||||
// host running MagicDNS it answers for base_domain itself instead of
|
||||
// asking the public zone.
|
||||
var defaultChallengeResolvers = []string{"1.1.1.1:53", "8.8.8.8:53"}
|
||||
|
||||
type IPAllocationStrategy string
|
||||
|
||||
const (
|
||||
@@ -139,6 +152,8 @@ type Config struct {
|
||||
// it can be used directly when sending Netmaps to clients.
|
||||
TailcfgDNSConfig *tailcfg.DNSConfig
|
||||
|
||||
DNSChallenge DNSChallengeConfig
|
||||
|
||||
UnixSocket string
|
||||
UnixSocketPermission fs.FileMode
|
||||
|
||||
@@ -163,6 +178,29 @@ type DNSConfig struct {
|
||||
SearchDomains []string `mapstructure:"search_domains"`
|
||||
ExtraRecords []tailcfg.DNSRecord `mapstructure:"extra_records"`
|
||||
ExtraRecordsPath string `mapstructure:"extra_records_path"`
|
||||
Certificates CertificatesConfig
|
||||
}
|
||||
|
||||
// CertificatesConfig lets nodes get TLS certificates for their MagicDNS
|
||||
// name with `tailscale cert`. The node runs ACME itself; headscale
|
||||
// publishes its DNS-01 challenge through [DNSChallengeConfig].
|
||||
type CertificatesConfig struct {
|
||||
Enabled bool
|
||||
// Zone is the public DNS zone challenge records are written to. It is
|
||||
// base_domain unless base_domain sits below the zone apex.
|
||||
Zone string
|
||||
}
|
||||
|
||||
// DNSChallengeConfig is the DNS provider answering ACME DNS-01
|
||||
// challenges for node certificates and headscale's own certificate.
|
||||
type DNSChallengeConfig struct {
|
||||
PropagationTimeout time.Duration
|
||||
Resolvers []string
|
||||
ProviderName string
|
||||
// Provider is nil when no provider is configured. It is built while
|
||||
// loading so `headscale configtest` reports bad provider settings,
|
||||
// and never serialised because it holds credentials.
|
||||
Provider hsdns.Provider `json:"-"`
|
||||
}
|
||||
|
||||
type Nameservers struct {
|
||||
@@ -449,6 +487,9 @@ func LoadConfig(path string, isFile bool) error {
|
||||
viper.SetDefault("dns.nameservers.global", []string{})
|
||||
viper.SetDefault("dns.nameservers.split", map[string]string{})
|
||||
viper.SetDefault("dns.search_domains", []string{})
|
||||
viper.SetDefault("dns.certificates.enabled", false)
|
||||
|
||||
viper.SetDefault("dns.challenge.propagation_timeout", "2m")
|
||||
|
||||
viper.SetDefault("derp.server.enabled", false)
|
||||
viper.SetDefault("derp.server.verify_clients", true)
|
||||
@@ -707,7 +748,7 @@ func validateServerConfigInto(v *configValidator) {
|
||||
{"dns.nameservers.global", "[]"},
|
||||
},
|
||||
Hint: "list at least one upstream nameserver, or set dns.override_local_dns: false",
|
||||
See: "https://headscale.net/stable/ref/dns/",
|
||||
See: dnsDocsURL,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -765,6 +806,7 @@ func validateServerConfigInto(v *configValidator) {
|
||||
validateDERPConfig(v)
|
||||
validateDatabaseConfig(v)
|
||||
validateMagicDNSConfig(v)
|
||||
validateCertificatesConfig(v)
|
||||
|
||||
depr.Apply(v)
|
||||
}
|
||||
@@ -834,11 +876,110 @@ func validateMagicDNSConfig(v *configValidator) {
|
||||
{"dns.base_domain", ""},
|
||||
},
|
||||
Hint: `set dns.base_domain to a domain you control (e.g. "ts.example.net"), or set dns.magic_dns: false`,
|
||||
See: "https://headscale.net/stable/ref/dns/",
|
||||
See: dnsDocsURL,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// validateCertificatesConfig records ConfigErrors when node certificates
|
||||
// are enabled without what the Tailscale client needs: a MagicDNS name
|
||||
// it can match exactly, and a provider to publish the challenge.
|
||||
func validateCertificatesConfig(v *configValidator) {
|
||||
if timeout := viper.GetDuration("dns.challenge.propagation_timeout"); timeout < 0 {
|
||||
v.Add(&ConfigError{
|
||||
Reason: "dns.challenge.propagation_timeout must not be negative",
|
||||
Current: []KV{{"dns.challenge.propagation_timeout", timeout.String()}},
|
||||
Hint: "use 0 to skip waiting for the record on the zone's nameservers",
|
||||
})
|
||||
}
|
||||
|
||||
if !viper.GetBool("dns.certificates.enabled") {
|
||||
return
|
||||
}
|
||||
|
||||
if !viper.GetBool("dns.magic_dns") {
|
||||
v.Add(&ConfigError{
|
||||
Reason: "dns.certificates requires dns.magic_dns",
|
||||
Current: []KV{{"dns.certificates.enabled", true}, {"dns.magic_dns", false}},
|
||||
Hint: "certificates are issued for MagicDNS names; set dns.magic_dns: true",
|
||||
See: dnsDocsURL,
|
||||
})
|
||||
}
|
||||
|
||||
if viper.GetString("dns.challenge.provider.name") == "" {
|
||||
v.Add(&ConfigError{
|
||||
Reason: "dns.certificates requires dns.challenge.provider",
|
||||
Current: []KV{{"dns.certificates.enabled", true}},
|
||||
Allowed: hsdns.ProviderNames(),
|
||||
Hint: "configure the DNS provider that hosts dns.base_domain under dns.challenge.provider",
|
||||
See: dnsDocsURL,
|
||||
})
|
||||
}
|
||||
|
||||
base := strings.TrimSuffix(viper.GetString("dns.base_domain"), ".")
|
||||
if base != strings.ToLower(base) || dnsname.ValidHostname(base) != nil {
|
||||
v.Add(&ConfigError{
|
||||
Reason: "dns.base_domain must be a lowercase DNS name for dns.certificates",
|
||||
Current: []KV{{"dns.base_domain", base}},
|
||||
Hint: "Tailscale clients match certificate names exactly; use a lowercase public domain you control",
|
||||
See: dnsDocsURL,
|
||||
})
|
||||
}
|
||||
|
||||
zone := strings.TrimSuffix(viper.GetString("dns.certificates.zone"), ".")
|
||||
if zone != "" && base != zone && !strings.HasSuffix(base, "."+zone) {
|
||||
v.Add(&ConfigError{
|
||||
Reason: "dns.base_domain is not inside dns.certificates.zone",
|
||||
Current: []KV{{"dns.base_domain", base}, {"dns.certificates.zone", zone}},
|
||||
Hint: "set zone to the DNS zone that contains base_domain, or leave it empty to use base_domain",
|
||||
See: dnsDocsURL,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// dnsChallengeConfig builds the DNS-01 provider. Provider keys are read
|
||||
// one by one, because environment variables never reach into a map.
|
||||
func dnsChallengeConfig() (DNSChallengeConfig, error) {
|
||||
cfg := DNSChallengeConfig{
|
||||
PropagationTimeout: viper.GetDuration("dns.challenge.propagation_timeout"),
|
||||
Resolvers: viper.GetStringSlice("dns.challenge.resolvers"),
|
||||
ProviderName: viper.GetString("dns.challenge.provider.name"),
|
||||
}
|
||||
|
||||
if len(cfg.Resolvers) == 0 {
|
||||
cfg.Resolvers = slices.Clone(defaultChallengeResolvers)
|
||||
}
|
||||
|
||||
// Like dns.nameservers, a resolver may be a bare IP.
|
||||
for i, r := range cfg.Resolvers {
|
||||
_, _, err := net.SplitHostPort(r)
|
||||
if err != nil {
|
||||
cfg.Resolvers[i] = net.JoinHostPort(r, "53")
|
||||
}
|
||||
}
|
||||
|
||||
if cfg.ProviderName == "" {
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
provider, err := hsdns.NewProvider(
|
||||
cfg.ProviderName,
|
||||
viper.GetStringMap("dns.challenge.provider"),
|
||||
func(key string) (any, bool) {
|
||||
key = "dns.challenge.provider." + key
|
||||
|
||||
return viper.Get(key), viper.IsSet(key)
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return cfg, err
|
||||
}
|
||||
|
||||
cfg.Provider = provider
|
||||
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func tlsConfig() TLSConfig {
|
||||
return TLSConfig{
|
||||
LetsEncrypt: LetsEncryptConfig{
|
||||
@@ -1032,6 +1173,8 @@ func dns() (DNSConfig, error) {
|
||||
dns.Nameservers.Split = viper.GetStringMapStringSlice("dns.nameservers.split")
|
||||
dns.SearchDomains = viper.GetStringSlice("dns.search_domains")
|
||||
dns.ExtraRecordsPath = viper.GetString("dns.extra_records_path")
|
||||
dns.Certificates.Enabled = viper.GetBool("dns.certificates.enabled")
|
||||
dns.Certificates.Zone = cmp.Or(viper.GetString("dns.certificates.zone"), dns.BaseDomain)
|
||||
|
||||
if viper.IsSet("dns.extra_records") {
|
||||
var extraRecords []tailcfg.DNSRecord
|
||||
@@ -1344,6 +1487,25 @@ func LoadServerConfig() (*Config, error) {
|
||||
derpConfig := derpConfig()
|
||||
logTailConfig := logtailConfig()
|
||||
|
||||
dnsChallenge, err := dnsChallengeConfig()
|
||||
if err != nil {
|
||||
// The provider names only help when the name is what is wrong.
|
||||
var allowed []string
|
||||
if errors.Is(err, hsdns.ErrUnknownProvider) {
|
||||
allowed = hsdns.ProviderNames()
|
||||
}
|
||||
|
||||
v.Add(&ConfigError{
|
||||
Reason: "dns.challenge.provider is invalid",
|
||||
Current: []KV{{"dns.challenge.provider.name", dnsChallenge.ProviderName}},
|
||||
Detail: err.Error(),
|
||||
Allowed: allowed,
|
||||
Hint: "check the provider name and its settings; any string setting can be read from a file with <setting>_path",
|
||||
See: dnsDocsURL,
|
||||
Cause: err,
|
||||
})
|
||||
}
|
||||
|
||||
oidcClientSecret := viper.GetString("oidc.client_secret")
|
||||
oidcClientSecretPath := viper.GetString("oidc.client_secret_path")
|
||||
|
||||
@@ -1437,6 +1599,7 @@ func LoadServerConfig() (*Config, error) {
|
||||
|
||||
DNSConfig: dnsConfig,
|
||||
TailcfgDNSConfig: dnsToTailcfgDNS(dnsConfig),
|
||||
DNSChallenge: dnsChallenge,
|
||||
|
||||
ACMEEmail: viper.GetString("acme_email"),
|
||||
ACMEURL: viper.GetString("acme_url"),
|
||||
@@ -1519,7 +1682,7 @@ func addServerURLConfigError(v *configValidator, serverURL, baseDomain string, e
|
||||
Reason: "server_url and dns.base_domain refer to the same hostname",
|
||||
Current: current,
|
||||
Hint: "give server_url a host distinct from dns.base_domain (Tailscale takes over base_domain via MagicDNS)",
|
||||
See: "https://headscale.net/stable/ref/dns/",
|
||||
See: dnsDocsURL,
|
||||
Cause: errServerURLSame,
|
||||
})
|
||||
|
||||
@@ -1528,7 +1691,7 @@ func addServerURLConfigError(v *configValidator, serverURL, baseDomain string, e
|
||||
Reason: "server_url is a subdomain of dns.base_domain",
|
||||
Current: current,
|
||||
Hint: "host headscale on a domain outside dns.base_domain (Tailscale takes over base_domain via MagicDNS)",
|
||||
See: "https://headscale.net/stable/ref/dns/",
|
||||
See: dnsDocsURL,
|
||||
Cause: errServerURLSuffix,
|
||||
})
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/google/go-cmp/cmp/cmpopts"
|
||||
"github.com/juanfont/headscale/hscontrol/util"
|
||||
"github.com/libdns/cloudflare"
|
||||
"github.com/spf13/viper"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -61,6 +62,7 @@ func TestReadConfig(t *testing.T) {
|
||||
{Name: "prometheus.myvpn.example.com", Type: "A", Value: "100.64.0.4"},
|
||||
},
|
||||
SearchDomains: []string{"test.com", "bar.com"},
|
||||
Certificates: CertificatesConfig{Zone: "example.com"},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -127,6 +129,7 @@ func TestReadConfig(t *testing.T) {
|
||||
{Name: "prometheus.myvpn.example.com", Type: "A", Value: "100.64.0.4"},
|
||||
},
|
||||
SearchDomains: []string{"test.com", "bar.com"},
|
||||
Certificates: CertificatesConfig{Zone: "example.com"},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -331,6 +334,7 @@ func TestReadConfigFromEnv(t *testing.T) {
|
||||
// {Name: "prometheus.myvpn.example.com", Type: "A", Value: "100.64.0.4"},
|
||||
// },
|
||||
SearchDomains: []string{"test.com", "bar.com"},
|
||||
Certificates: CertificatesConfig{Zone: "example.com"},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -873,6 +877,7 @@ func TestConfigJSONOmitsSecrets(t *testing.T) {
|
||||
secretPostgresPass = "p0stgres-secret-marker"
|
||||
secretClientSecret = "oidc-client-secret-marker" //nolint:gosec // test marker, not a real credential
|
||||
secretAPIKey = "headscale-cli-api-key-marker" //nolint:gosec // test marker, not a real credential
|
||||
secretDNSToken = "dns-provider-token-marker" //nolint:gosec // test marker, not a real credential
|
||||
)
|
||||
|
||||
cfg := &Config{
|
||||
@@ -887,13 +892,17 @@ func TestConfigJSONOmitsSecrets(t *testing.T) {
|
||||
CLI: CLIConfig{
|
||||
APIKey: secretAPIKey,
|
||||
},
|
||||
DNSChallenge: DNSChallengeConfig{
|
||||
ProviderName: "cloudflare",
|
||||
Provider: &cloudflare.Provider{APIToken: secretDNSToken},
|
||||
},
|
||||
}
|
||||
|
||||
out, err := json.Marshal(cfg)
|
||||
require.NoError(t, err)
|
||||
|
||||
body := string(out)
|
||||
for _, secret := range []string{secretPostgresPass, secretClientSecret, secretAPIKey} {
|
||||
for _, secret := range []string{secretPostgresPass, secretClientSecret, secretAPIKey, secretDNSToken} {
|
||||
assert.NotContains(t, body, secret,
|
||||
"marshalled Config must not contain secret %q", secret)
|
||||
}
|
||||
@@ -1017,3 +1026,201 @@ func TestExtraRecordsAreLowercased(t *testing.T) {
|
||||
t.Errorf("SetExtraRecords mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDNSChallengeConfig(t *testing.T) {
|
||||
secret := filepath.Join(t.TempDir(), "token")
|
||||
require.NoError(t, os.WriteFile(secret, []byte("file-token\n"), 0o600))
|
||||
|
||||
base := `---
|
||||
server_url: https://headscale.example.com
|
||||
noise:
|
||||
private_key_path: noise_private.key
|
||||
prefixes:
|
||||
v4: 100.64.0.0/10
|
||||
database:
|
||||
type: sqlite
|
||||
dns:
|
||||
magic_dns: true
|
||||
override_local_dns: false
|
||||
base_domain: ts.example.com
|
||||
`
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
yaml string
|
||||
env map[string]string
|
||||
wantErr string
|
||||
wantZone string
|
||||
wantToken string
|
||||
wantNoProv bool
|
||||
wantResolvers []string
|
||||
}{
|
||||
{
|
||||
name: "disabled by default",
|
||||
yaml: base,
|
||||
// Zone follows base_domain even when unused, so it is never empty.
|
||||
wantZone: "ts.example.com",
|
||||
wantNoProv: true,
|
||||
wantResolvers: []string{"1.1.1.1:53", "8.8.8.8:53"},
|
||||
},
|
||||
{
|
||||
name: "resolvers default to port 53",
|
||||
yaml: base + ` challenge:
|
||||
resolvers: ["9.9.9.9", "2620:fe::fe", "127.0.0.1:5353"]
|
||||
`,
|
||||
wantZone: "ts.example.com",
|
||||
wantNoProv: true,
|
||||
wantResolvers: []string{"9.9.9.9:53", "[2620:fe::fe]:53", "127.0.0.1:5353"},
|
||||
},
|
||||
{
|
||||
name: "enabled with inline token",
|
||||
yaml: base + ` certificates:
|
||||
enabled: true
|
||||
challenge:
|
||||
provider:
|
||||
name: cloudflare
|
||||
api_token: yaml-token
|
||||
`,
|
||||
wantZone: "ts.example.com",
|
||||
wantToken: "yaml-token",
|
||||
},
|
||||
{
|
||||
name: "env only",
|
||||
yaml: base,
|
||||
env: map[string]string{
|
||||
"HEADSCALE_DNS_CERTIFICATES_ENABLED": "true",
|
||||
"HEADSCALE_DNS_CERTIFICATES_ZONE": "example.com",
|
||||
"HEADSCALE_DNS_CHALLENGE_PROVIDER_NAME": "cloudflare",
|
||||
"HEADSCALE_DNS_CHALLENGE_PROVIDER_API_TOKEN_PATH": secret,
|
||||
},
|
||||
wantZone: "example.com",
|
||||
wantToken: "file-token",
|
||||
},
|
||||
{
|
||||
name: "env overrides yaml",
|
||||
yaml: base + ` challenge:
|
||||
provider:
|
||||
name: cloudflare
|
||||
api_token: yaml-token
|
||||
`,
|
||||
env: map[string]string{"HEADSCALE_DNS_CHALLENGE_PROVIDER_API_TOKEN": "env-token"},
|
||||
wantZone: "ts.example.com",
|
||||
wantToken: "env-token",
|
||||
},
|
||||
{
|
||||
name: "typo in provider key",
|
||||
yaml: base + ` challenge:
|
||||
provider:
|
||||
name: cloudflare
|
||||
auth_token: tok
|
||||
`,
|
||||
wantErr: "dns.challenge.provider is invalid",
|
||||
},
|
||||
{
|
||||
name: "unknown provider",
|
||||
yaml: base + ` challenge:
|
||||
provider:
|
||||
name: route53
|
||||
`,
|
||||
wantErr: "dns.challenge.provider is invalid",
|
||||
},
|
||||
{
|
||||
name: "enabled without magic dns",
|
||||
yaml: strings.Replace(base, "magic_dns: true", "magic_dns: false", 1) + ` certificates:
|
||||
enabled: true
|
||||
challenge:
|
||||
provider:
|
||||
name: cloudflare
|
||||
api_token: tok
|
||||
`,
|
||||
wantErr: "dns.certificates requires dns.magic_dns",
|
||||
},
|
||||
{
|
||||
name: "enabled without provider",
|
||||
yaml: base + ` certificates:
|
||||
enabled: true
|
||||
`,
|
||||
wantErr: "dns.certificates requires dns.challenge.provider",
|
||||
},
|
||||
{
|
||||
name: "base_domain outside zone",
|
||||
yaml: base + ` certificates:
|
||||
enabled: true
|
||||
zone: notexample.com
|
||||
challenge:
|
||||
provider:
|
||||
name: cloudflare
|
||||
api_token: tok
|
||||
`,
|
||||
wantErr: "dns.base_domain is not inside dns.certificates.zone",
|
||||
},
|
||||
{
|
||||
name: "zone suffix without label boundary",
|
||||
yaml: base + ` certificates:
|
||||
enabled: true
|
||||
zone: ample.com
|
||||
challenge:
|
||||
provider:
|
||||
name: cloudflare
|
||||
api_token: tok
|
||||
`,
|
||||
wantErr: "dns.base_domain is not inside dns.certificates.zone",
|
||||
},
|
||||
{
|
||||
name: "uppercase base_domain",
|
||||
yaml: strings.Replace(base, "ts.example.com", "TS.example.com", 1) + ` certificates:
|
||||
enabled: true
|
||||
challenge:
|
||||
provider:
|
||||
name: cloudflare
|
||||
api_token: tok
|
||||
`,
|
||||
wantErr: "dns.base_domain must be a lowercase DNS name",
|
||||
},
|
||||
{
|
||||
name: "negative propagation timeout",
|
||||
yaml: base + ` challenge:
|
||||
propagation_timeout: -1s
|
||||
`,
|
||||
wantErr: "dns.challenge.propagation_timeout must not be negative",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
viper.Reset()
|
||||
|
||||
for k, v := range tt.env {
|
||||
t.Setenv(k, v)
|
||||
}
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
require.NoError(t, os.WriteFile(filepath.Join(tmpDir, "config.yaml"), []byte(tt.yaml), 0o600))
|
||||
require.NoError(t, LoadConfig(tmpDir, false))
|
||||
|
||||
cfg, err := LoadServerConfig()
|
||||
if tt.wantErr != "" {
|
||||
require.ErrorContains(t, err, tt.wantErr)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tt.wantZone, cfg.DNSConfig.Certificates.Zone)
|
||||
|
||||
if tt.wantResolvers != nil {
|
||||
assert.Equal(t, tt.wantResolvers, cfg.DNSChallenge.Resolvers)
|
||||
}
|
||||
|
||||
if tt.wantNoProv {
|
||||
assert.Nil(t, cfg.DNSChallenge.Provider)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
provider, ok := cfg.DNSChallenge.Provider.(*cloudflare.Provider)
|
||||
require.True(t, ok, "provider is %T", cfg.DNSChallenge.Provider)
|
||||
assert.Equal(t, tt.wantToken, provider.APIToken)
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user