mirror of
https://github.com/juanfont/headscale.git
synced 2026-10-28 17:26:52 +09:00
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:
1 parent
ee47638787
commit
dcc01dc2b0
7 files changed
+279
-106
No files matched your search
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user