types: move TLS config under tls with legacy fallback

tls.cert_path, tls.key_path and tls.acme.* replace the flat keys, which
still work. Warn on them only when they turn TLS on: NixOS always sets them.

Updates #2527
This commit is contained in:
Kristoffer Dalby committed 2026-10-18 20:53:18 +02:00
1 parent ee47638787
commit dcc01dc2b0
7 files changed
+279 -106

No files matched your search

+3 -2
View File
@@ -83,8 +83,9 @@ runs:
nameservers:
global:
- 1.1.1.1
tls_cert_path: /tmp/hs/tls.crt
tls_key_path: /tmp/hs/tls.key
tls:
cert_path: /tmp/hs/tls.crt
key_path: /tmp/hs/tls.key
policy:
mode: file
path: /tmp/hs/policy.hujson
+6 -6
View File
@@ -38,9 +38,9 @@ func TestConfigFileLoading(t *testing.T) {
assert.Equal(t, "127.0.0.1:9090", viper.GetString("metrics_listen_addr"))
assert.Equal(t, "sqlite", viper.GetString("database.type"))
assert.Equal(t, "/var/lib/headscale/db.sqlite", viper.GetString("database.sqlite.path"))
assert.Empty(t, viper.GetString("tls_letsencrypt_hostname"))
assert.Equal(t, ":http", viper.GetString("tls_letsencrypt_listen"))
assert.Equal(t, "HTTP-01", viper.GetString("tls_letsencrypt_challenge_type"))
assert.Empty(t, viper.GetString("tls.acme.hostname"))
assert.Equal(t, ":http", viper.GetString("tls.acme.http_listen"))
assert.Equal(t, "HTTP-01", viper.GetString("tls.acme.challenge_type"))
mode, err := util.ParseFileMode(viper.GetString("unix_socket_permission"))
require.NoError(t, err)
assert.Equal(t, fs.FileMode(0o770), mode)
@@ -70,9 +70,9 @@ func TestConfigLoading(t *testing.T) {
assert.Equal(t, "127.0.0.1:9090", viper.GetString("metrics_listen_addr"))
assert.Equal(t, "sqlite", viper.GetString("database.type"))
assert.Equal(t, "/var/lib/headscale/db.sqlite", viper.GetString("database.sqlite.path"))
assert.Empty(t, viper.GetString("tls_letsencrypt_hostname"))
assert.Equal(t, ":http", viper.GetString("tls_letsencrypt_listen"))
assert.Equal(t, "HTTP-01", viper.GetString("tls_letsencrypt_challenge_type"))
assert.Empty(t, viper.GetString("tls.acme.hostname"))
assert.Equal(t, ":http", viper.GetString("tls.acme.http_listen"))
assert.Equal(t, "HTTP-01", viper.GetString("tls.acme.challenge_type"))
mode, err := util.ParseFileMode(viper.GetString("unix_socket_permission"))
require.NoError(t, err)
assert.Equal(t, fs.FileMode(0o770), mode)
+23 -23
View File
@@ -232,35 +232,35 @@ database:
### TLS configuration
# See: https://headscale.net/stable/ref/tls/
tls:
## Use already defined certificates:
cert_path: ""
key_path: ""
## Let's Encrypt / ACME
# Headscale supports automatically requesting and setting up
# TLS for a domain with Let's Encrypt.
## Let's Encrypt / ACME
# Headscale supports automatically requesting and setting up
# TLS for a domain with Let's Encrypt.
acme:
# Domain name to request a TLS certificate for:
hostname: ""
# URL to ACME directory
acme_url: https://acme-v02.api.letsencrypt.org/directory
# Type of ACME challenge to use, currently supported types:
# HTTP-01 or TLS-ALPN-01
challenge_type: HTTP-01
# Email to register with ACME provider
acme_email: ""
# When HTTP-01 challenge is chosen, letsencrypt must set up a
# verification endpoint, and it will be listening on:
# :http = port 80
http_listen: ":http"
# Domain name to request a TLS certificate for:
tls_letsencrypt_hostname: ""
# URL to ACME directory
url: https://acme-v02.api.letsencrypt.org/directory
# Path to store certificates and metadata needed by letsencrypt
tls_letsencrypt_cache_dir: /var/lib/headscale/cache
# Email to register with ACME provider
email: ""
# Type of ACME challenge to use, currently supported types:
# HTTP-01 or TLS-ALPN-01
tls_letsencrypt_challenge_type: HTTP-01
# When HTTP-01 challenge is chosen, letsencrypt must set up a
# verification endpoint, and it will be listening on:
# :http = port 80
tls_letsencrypt_listen: ":http"
## Use already defined certificates:
tls_cert_path: ""
tls_key_path: ""
# Path to store certificates and metadata needed by letsencrypt
cache_dir: /var/lib/headscale/cache
log:
# Valid log levels: panic, fatal, error, warn, info, debug, trace
+10 -10
View File
@@ -1022,30 +1022,30 @@ type tlsBundle struct {
}
func (h *Headscale) getTLSSettings(ctx context.Context) (*tlsBundle, error) {
tlsEnabled := h.cfg.TLS.LetsEncrypt.Hostname != "" || h.cfg.TLS.CertPath != ""
tlsEnabled := h.cfg.TLS.ACME.Hostname != "" || h.cfg.TLS.CertPath != ""
if tlsEnabled && !strings.HasPrefix(h.cfg.ServerURL, "https://") {
log.Warn().Msg("listening with TLS but ServerURL does not start with https://")
} else if !tlsEnabled && !strings.HasPrefix(h.cfg.ServerURL, "http://") {
log.Warn().Msg("listening without TLS but ServerURL does not start with http://")
}
if h.cfg.TLS.LetsEncrypt.Hostname != "" {
if h.cfg.TLS.ACME.Hostname != "" {
certManager := autocert.Manager{
Prompt: autocert.AcceptTOS,
HostPolicy: autocert.HostWhitelist(h.cfg.TLS.LetsEncrypt.Hostname),
Cache: autocert.DirCache(h.cfg.TLS.LetsEncrypt.CacheDir),
HostPolicy: autocert.HostWhitelist(h.cfg.TLS.ACME.Hostname),
Cache: autocert.DirCache(h.cfg.TLS.ACME.CacheDir),
Client: &acme.Client{
DirectoryURL: h.cfg.ACMEURL,
DirectoryURL: h.cfg.TLS.ACME.URL,
HTTPClient: &http.Client{
Transport: &acmeLogger{
rt: http.DefaultTransport,
},
},
},
Email: h.cfg.ACMEEmail,
Email: h.cfg.TLS.ACME.Email,
}
switch h.cfg.TLS.LetsEncrypt.ChallengeType {
switch h.cfg.TLS.ACME.ChallengeType {
case types.TLSALPN01ChallengeType:
// Configuration via autocert with TLS-ALPN-01 (https://tools.ietf.org/html/rfc8737)
// The RFC requires that the validation is done on port 443; in other words, headscale
@@ -1057,7 +1057,7 @@ func (h *Headscale) getTLSSettings(ctx context.Context) (*tlsBundle, error) {
// port 80 for the certificate validation in addition to the headscale
// service, which can be configured to run on any other port.
server := &http.Server{
Addr: h.cfg.TLS.LetsEncrypt.Listen,
Addr: h.cfg.TLS.ACME.HTTPListen,
Handler: certManager.HTTPHandler(http.HandlerFunc(h.redirect)),
ReadTimeout: types.HTTPTimeout,
}
@@ -1066,7 +1066,7 @@ func (h *Headscale) getTLSSettings(ctx context.Context) (*tlsBundle, error) {
if err != nil {
return nil, &types.ListenerBindError{
Listener: "ACME HTTP-01 challenge",
YAMLKey: "tls_letsencrypt_listen",
YAMLKey: "tls.acme.http_listen",
Addr: server.Addr,
Err: err,
}
@@ -1089,7 +1089,7 @@ func (h *Headscale) getTLSSettings(ctx context.Context) (*tlsBundle, error) {
cert, err := tls.LoadX509KeyPair(h.cfg.TLS.CertPath, h.cfg.TLS.KeyPath)
if err != nil {
return nil, fmt.Errorf("loading TLS keypair (tls_cert_path=%q, tls_key_path=%q): %w",
return nil, fmt.Errorf("loading TLS keypair (tls.cert_path=%q, tls.key_path=%q): %w",
h.cfg.TLS.CertPath, h.cfg.TLS.KeyPath, err)
}
+75 -40
View File
@@ -5,6 +5,7 @@ import (
"errors"
"fmt"
"io/fs"
"maps"
"net"
"net/netip"
"net/url"
@@ -140,9 +141,6 @@ type Config struct {
TLS TLSConfig
ACMEURL string
ACMEEmail string
// DNSConfig is the headscale representation of the DNS configuration.
// It is kept in the config update for some settings that are
// not directly converted into a [tailcfg.DNSConfig].
@@ -250,14 +248,60 @@ type TLSConfig struct {
CertPath string
KeyPath string
LetsEncrypt LetsEncryptConfig
ACME ACMEConfig
}
type LetsEncryptConfig struct {
Listen string
// ACMEConfig gets headscale's own certificate from an ACME CA.
type ACMEConfig struct {
Hostname string
CacheDir string
ChallengeType string
// HTTPListen serves HTTP-01 challenges.
HTTPListen string
URL string
Email string
CacheDir string
}
// legacyTLSKeys maps each tls key to the flat key it replaced. The flat
// keys keep working, so existing configs and NixOS modules still run.
var legacyTLSKeys = map[string]string{
"tls.cert_path": "tls_cert_path",
"tls.key_path": "tls_key_path",
"tls.acme.hostname": "tls_letsencrypt_hostname",
"tls.acme.challenge_type": "tls_letsencrypt_challenge_type",
"tls.acme.http_listen": "tls_letsencrypt_listen",
"tls.acme.cache_dir": "tls_letsencrypt_cache_dir",
"tls.acme.url": "acme_url",
"tls.acme.email": "acme_email",
}
var tlsDefaults = map[string]string{
"tls.acme.challenge_type": HTTP01ChallengeType,
"tls.acme.http_listen": ":http",
"tls.acme.cache_dir": "/var/www/.cache",
}
// warnLegacyTLSKeys points at the tls keys for flat keys in the config
// file that turn TLS on. NixOS writes the flat keys even with TLS off, and
// the HEADSCALE_TLS_* variables name both the flat and the tls keys.
func warnLegacyTLSKeys(depr *deprecator) {
legacyTLSOn := func(key string) bool { return viper.InConfig(key) && viper.GetString(key) != "" }
if !legacyTLSOn("tls_letsencrypt_hostname") && !legacyTLSOn("tls_cert_path") {
return
}
for _, key := range slices.Sorted(maps.Keys(legacyTLSKeys)) {
if viper.InConfig(legacyTLSKeys[key]) {
depr.warnNoAlias(key, legacyTLSKeys[key])
}
}
}
// tlsKey reads a tls setting, falling back to the flat key it replaced and
// then to its default. The defaults are not viper defaults: those would
// shadow the flat key.
func tlsKey(key string) string {
return cmp.Or(viper.GetString(key), viper.GetString(legacyTLSKeys[key]), tlsDefaults[key])
}
type PKCEConfig struct {
@@ -474,10 +518,6 @@ func LoadConfig(path string, isFile bool) error {
viper.SetDefault("policy.mode", "file")
viper.SetDefault("tls_letsencrypt_cache_dir", "/var/www/.cache")
viper.SetDefault("tls_letsencrypt_challenge_type", HTTP01ChallengeType)
viper.SetDefault("tls_letsencrypt_listen", ":http")
viper.SetDefault("log.level", "info")
viper.SetDefault("log.format", TextLogFormat)
@@ -675,14 +715,16 @@ func validateServerConfigInto(v *configValidator) {
})
}
if (viper.GetString("tls_letsencrypt_hostname") != "") &&
((viper.GetString("tls_cert_path") != "") || (viper.GetString("tls_key_path") != "")) {
warnLegacyTLSKeys(&depr)
if tlsKey("tls.acme.hostname") != "" &&
(tlsKey("tls.cert_path") != "" || tlsKey("tls.key_path") != "") {
v.Add(&ConfigError{
Reason: "tls_letsencrypt_hostname and tls_cert_path/tls_key_path are mutually exclusive",
Current: []KV{{"tls_letsencrypt_hostname", viper.GetString("tls_letsencrypt_hostname")}},
Reason: "tls.acme.hostname and tls.cert_path/tls.key_path are mutually exclusive",
Current: []KV{{"tls.acme.hostname", tlsKey("tls.acme.hostname")}},
ConflictsWith: []KV{
{"tls_cert_path", viper.GetString("tls_cert_path")},
{"tls_key_path", viper.GetString("tls_key_path")},
{"tls.cert_path", tlsKey("tls.cert_path")},
{"tls.key_path", tlsKey("tls.key_path")},
},
Hint: "choose one TLS strategy and unset the other (Let's Encrypt OR a static keypair)",
See: "https://headscale.net/stable/ref/tls/",
@@ -697,20 +739,20 @@ func validateServerConfigInto(v *configValidator) {
})
}
if (viper.GetString("tls_letsencrypt_hostname") != "") &&
(viper.GetString("tls_letsencrypt_challenge_type") == TLSALPN01ChallengeType) &&
(!strings.HasSuffix(viper.GetString("listen_addr"), ":443")) {
if tlsKey("tls.acme.hostname") != "" &&
tlsKey("tls.acme.challenge_type") == TLSALPN01ChallengeType &&
!strings.HasSuffix(viper.GetString("listen_addr"), ":443") {
// this is only a warning because there could be something sitting in front of headscale that redirects the traffic (e.g. an iptables rule)
log.Warn().
Msg("Warning: when using tls_letsencrypt_hostname with TLS-ALPN-01 as challenge type, headscale must be reachable on port 443, i.e. listen_addr should probably end in :443")
Msg("Warning: when using tls.acme.hostname with TLS-ALPN-01 as challenge type, headscale must be reachable on port 443, i.e. listen_addr should probably end in :443")
}
validateListenerCollisions(v)
if ct := viper.GetString("tls_letsencrypt_challenge_type"); ct != HTTP01ChallengeType && ct != TLSALPN01ChallengeType {
if ct := tlsKey("tls.acme.challenge_type"); ct != HTTP01ChallengeType && ct != TLSALPN01ChallengeType {
v.Add(&ConfigError{
Reason: "tls_letsencrypt_challenge_type has an unsupported value",
Current: []KV{{"tls_letsencrypt_challenge_type", ct}},
Reason: "tls.acme.challenge_type has an unsupported value",
Current: []KV{{"tls.acme.challenge_type", ct}},
Allowed: []string{HTTP01ChallengeType, TLSALPN01ChallengeType},
Hint: "pick one of the allowed values; HTTP-01 is the default",
})
@@ -989,20 +1031,16 @@ func dnsChallengeConfig() (DNSChallengeConfig, error) {
func tlsConfig() TLSConfig {
return TLSConfig{
LetsEncrypt: LetsEncryptConfig{
Hostname: viper.GetString("tls_letsencrypt_hostname"),
Listen: viper.GetString("tls_letsencrypt_listen"),
CacheDir: util.AbsolutePathFromConfigPath(
viper.GetString("tls_letsencrypt_cache_dir"),
),
ChallengeType: viper.GetString("tls_letsencrypt_challenge_type"),
CertPath: util.AbsolutePathFromConfigPath(tlsKey("tls.cert_path")),
KeyPath: util.AbsolutePathFromConfigPath(tlsKey("tls.key_path")),
ACME: ACMEConfig{
Hostname: tlsKey("tls.acme.hostname"),
ChallengeType: tlsKey("tls.acme.challenge_type"),
HTTPListen: tlsKey("tls.acme.http_listen"),
URL: tlsKey("tls.acme.url"),
Email: tlsKey("tls.acme.email"),
CacheDir: util.AbsolutePathFromConfigPath(tlsKey("tls.acme.cache_dir")),
},
CertPath: util.AbsolutePathFromConfigPath(
viper.GetString("tls_cert_path"),
),
KeyPath: util.AbsolutePathFromConfigPath(
viper.GetString("tls_key_path"),
),
}
}
@@ -1608,9 +1646,6 @@ func LoadServerConfig() (*Config, error) {
TailcfgDNSConfig: dnsToTailcfgDNS(dnsConfig),
DNSChallenge: dnsChallenge,
ACMEEmail: viper.GetString("acme_email"),
ACMEURL: viper.GetString("acme_url"),
UnixSocket: viper.GetString("unix_socket"),
UnixSocketPermission: socketPerm,
+158 -21
View File
@@ -367,9 +367,11 @@ func TestTLSConfigValidation(t *testing.T) {
var err error
configYaml := []byte(`---
tls_letsencrypt_hostname: example.com
tls_letsencrypt_challenge_type: ""
tls_cert_path: abc.pem
tls:
cert_path: abc.pem
acme:
hostname: example.com
challenge_type: bogus
noise:
private_key_path: noise_private.key`)
@@ -390,12 +392,12 @@ noise:
assert.Contains(
t,
err.Error(),
"Fatal config error: tls_letsencrypt_hostname and tls_cert_path/tls_key_path are mutually exclusive",
"Fatal config error: tls.acme.hostname and tls.cert_path/tls.key_path are mutually exclusive",
)
assert.Contains(
t,
err.Error(),
"Fatal config error: tls_letsencrypt_challenge_type has an unsupported value",
"Fatal config error: tls.acme.challenge_type has an unsupported value",
)
assert.Contains(
t,
@@ -408,8 +410,10 @@ noise:
noise:
private_key_path: noise_private.key
server_url: http://127.0.0.1:8080
tls_letsencrypt_hostname: example.com
tls_letsencrypt_challenge_type: TLS-ALPN-01
tls:
acme:
hostname: example.com
challenge_type: TLS-ALPN-01
`)
err = os.WriteFile(configFilePath, configYaml, 0o600)
@@ -523,22 +527,37 @@ oidc:` + tt.oidcBlock + "\n")
}
}
func TestTLSLetsEncryptListenAddrCollision(t *testing.T) {
func TestTLSACMEListenAddrCollision(t *testing.T) {
const (
tlsKeys = `tls:
acme:
hostname: %q
challenge_type: %q
http_listen: %q
`
legacyKeys = `tls_letsencrypt_hostname: %q
tls_letsencrypt_challenge_type: %q
tls_letsencrypt_listen: %q
`
)
tests := []struct {
name string
listenAddr string
leListen string
acmeListen string
hostname string
challengeType string
keys string
wantErr string // empty = expect no error
}{
{"collision-numeric", ":80", ":80", "example.com", "HTTP-01", "would bind the same TCP socket"},
{"collision-named-vs-numeric", "0.0.0.0:80", ":http", "example.com", "HTTP-01", "would bind the same TCP socket"},
{"collision-https-named", ":443", ":https", "example.com", "HTTP-01", "would bind the same TCP socket"},
{"canonical", "0.0.0.0:443", ":http", "example.com", "HTTP-01", ""},
{"no-hostname-skipped", "0.0.0.0:80", ":http", "", "HTTP-01", ""},
{"tls-alpn-01-skipped", "0.0.0.0:80", ":http", "example.com", "TLS-ALPN-01", ""},
{"different-numeric", ":8080", ":8081", "example.com", "HTTP-01", ""},
{"collision-numeric", ":80", ":80", "example.com", "HTTP-01", tlsKeys, "would bind the same TCP socket"},
{"collision-named-vs-numeric", "0.0.0.0:80", ":http", "example.com", "HTTP-01", tlsKeys, "would bind the same TCP socket"},
{"collision-https-named", ":443", ":https", "example.com", "HTTP-01", tlsKeys, "would bind the same TCP socket"},
{"collision-legacy-keys", ":80", ":80", "example.com", "HTTP-01", legacyKeys, "would bind the same TCP socket"},
{"canonical", "0.0.0.0:443", ":http", "example.com", "HTTP-01", tlsKeys, ""},
{"no-hostname-skipped", "0.0.0.0:80", ":http", "", "HTTP-01", tlsKeys, ""},
{"tls-alpn-01-skipped", "0.0.0.0:80", ":http", "example.com", "TLS-ALPN-01", tlsKeys, ""},
{"different-numeric", ":8080", ":8081", "example.com", "HTTP-01", tlsKeys, ""},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
@@ -548,17 +567,14 @@ func TestTLSLetsEncryptListenAddrCollision(t *testing.T) {
cfg := fmt.Sprintf(`---
server_url: https://example.com
listen_addr: %q
tls_letsencrypt_hostname: %q
tls_letsencrypt_challenge_type: %q
tls_letsencrypt_listen: %q
noise:
`+tt.keys+`noise:
private_key_path: noise_private.key
database:
type: sqlite3
dns:
magic_dns: false
override_local_dns: false
`, tt.listenAddr, tt.hostname, tt.challengeType, tt.leListen)
`, tt.listenAddr, tt.hostname, tt.challengeType, tt.acmeListen)
require.NoError(t, os.WriteFile(
filepath.Join(tmpDir, "config.yaml"), []byte(cfg), 0o600))
require.NoError(t, LoadConfig(tmpDir, false))
@@ -1291,3 +1307,124 @@ dns:
})
}
}
// TestTLSLegacyKeys pins that the flat TLS keys keep working under the tls
// block, with the tls keys winning, and that the NixOS module's defaults
// for the flat keys leave TLS off.
func TestTLSLegacyKeys(t *testing.T) {
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: false
override_local_dns: false
`
tests := []struct {
name string
yaml string
env map[string]string
want TLSConfig
}{
{
name: "defaults",
yaml: base,
want: TLSConfig{ACME: ACMEConfig{
ChallengeType: HTTP01ChallengeType,
HTTPListen: ":http",
CacheDir: "/var/www/.cache",
}},
},
{
name: "legacy keys",
yaml: base + `tls_letsencrypt_hostname: headscale.example.com
tls_letsencrypt_challenge_type: TLS-ALPN-01
tls_letsencrypt_listen: ":8080"
tls_letsencrypt_cache_dir: /var/lib/headscale/cache
acme_url: https://acme.example.com/directory
acme_email: admin@example.com
`,
want: TLSConfig{ACME: ACMEConfig{
Hostname: "headscale.example.com",
ChallengeType: TLSALPN01ChallengeType,
HTTPListen: ":8080",
URL: "https://acme.example.com/directory",
Email: "admin@example.com",
CacheDir: "/var/lib/headscale/cache",
}},
},
{
name: "tls keys win over legacy keys",
yaml: base + `tls_cert_path: /old/cert.pem
tls_key_path: /old/key.pem
tls:
cert_path: /new/cert.pem
key_path: /new/key.pem
`,
want: TLSConfig{
CertPath: "/new/cert.pem",
KeyPath: "/new/key.pem",
ACME: ACMEConfig{
ChallengeType: HTTP01ChallengeType,
HTTPListen: ":http",
CacheDir: "/var/www/.cache",
},
},
},
{
// What the NixOS module writes when TLS is off.
name: "nixos defaults",
yaml: base + `tls_letsencrypt_hostname: ""
tls_letsencrypt_challenge_type: HTTP-01
tls_letsencrypt_listen: ":http"
tls_letsencrypt_cache_dir: /var/lib/headscale/.cache
`,
want: TLSConfig{ACME: ACMEConfig{
ChallengeType: HTTP01ChallengeType,
HTTPListen: ":http",
CacheDir: "/var/lib/headscale/.cache",
}},
},
{
// Both keys read HEADSCALE_TLS_CERT_PATH.
name: "env",
yaml: base,
env: map[string]string{
"HEADSCALE_TLS_CERT_PATH": "/env/cert.pem",
"HEADSCALE_TLS_KEY_PATH": "/env/key.pem",
},
want: TLSConfig{
CertPath: "/env/cert.pem",
KeyPath: "/env/key.pem",
ACME: ACMEConfig{
ChallengeType: HTTP01ChallengeType,
HTTPListen: ":http",
CacheDir: "/var/www/.cache",
},
},
},
}
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()
require.NoError(t, err)
assert.Equal(t, tt.want, cfg.TLS)
})
}
}
+4 -4
View File
@@ -109,18 +109,18 @@ func validateListenerCollisions(v *configValidator) {
listenAddr := viper.GetString("listen_addr")
grpcAddr := viper.GetString("grpc_listen_addr")
metricsAddr := viper.GetString("metrics_listen_addr")
acmeAddr := viper.GetString("tls_letsencrypt_listen")
acmeAddr := tlsKey("tls.acme.http_listen")
listeners := []spec{
{key: "listen_addr", addr: listenAddr, active: listenAddr != ""},
{key: "grpc_listen_addr", addr: grpcAddr, active: grpcAddr != ""},
{key: "metrics_listen_addr", addr: metricsAddr, active: metricsAddr != ""},
{
key: "tls_letsencrypt_listen",
key: "tls.acme.http_listen",
addr: acmeAddr,
active: acmeAddr != "" &&
viper.GetString("tls_letsencrypt_hostname") != "" &&
viper.GetString("tls_letsencrypt_challenge_type") == HTTP01ChallengeType,
tlsKey("tls.acme.hostname") != "" &&
tlsKey("tls.acme.challenge_type") == HTTP01ChallengeType,
},
}