Compare commits

...

7 commits

Author SHA1 Message Date
Kedas
ca9938f4e9 fix: config aliases (spf13/viper#689) 2024-07-14 07:01:01 +00:00
Kedas
1fc14790f9
Merge branch 'juanfont:main' into main 2024-07-13 13:11:13 -07:00
Kedas
6d9e5b0022 feat: move configs (untested) 2024-07-13 10:31:05 +00:00
Kedas
9163edcf50 feat: add WithGrpcTLS shim 2024-05-25 05:35:00 +00:00
Mia
50a7315226
feat: move grpc config 2024-05-24 18:16:33 -07:00
Kedas
bbe1327785
Merge branch 'juanfont:main' into main 2024-05-24 14:44:25 -07:00
Kedas
e72bd1cc8c feat: add config to overwrite grpc certificate 2024-04-13 04:49:25 +00:00
8 changed files with 170 additions and 76 deletions

View file

@ -60,9 +60,9 @@ func (*Suite) TestConfigFileLoading(c *check.C) {
c.Assert(viper.GetString("metrics_listen_addr"), check.Equals, "127.0.0.1:9090")
c.Assert(viper.GetString("database.type"), check.Equals, "sqlite")
c.Assert(viper.GetString("database.sqlite.path"), check.Equals, "/var/lib/headscale/db.sqlite")
c.Assert(viper.GetString("tls_letsencrypt_hostname"), check.Equals, "")
c.Assert(viper.GetString("tls_letsencrypt_listen"), check.Equals, ":http")
c.Assert(viper.GetString("tls_letsencrypt_challenge_type"), check.Equals, "HTTP-01")
c.Assert(viper.GetString("tls.letsencrypt_hostname"), check.Equals, "")
c.Assert(viper.GetString("tls.letsencrypt_listen"), check.Equals, ":http")
c.Assert(viper.GetString("tls.letsencrypt_challenge_type"), check.Equals, "HTTP-01")
c.Assert(viper.GetStringSlice("dns_config.nameservers")[0], check.Equals, "1.1.1.1")
c.Assert(
util.GetFileMode("unix_socket_permission"),
@ -103,9 +103,9 @@ func (*Suite) TestConfigLoading(c *check.C) {
c.Assert(viper.GetString("metrics_listen_addr"), check.Equals, "127.0.0.1:9090")
c.Assert(viper.GetString("database.type"), check.Equals, "sqlite")
c.Assert(viper.GetString("database.sqlite.path"), check.Equals, "/var/lib/headscale/db.sqlite")
c.Assert(viper.GetString("tls_letsencrypt_hostname"), check.Equals, "")
c.Assert(viper.GetString("tls_letsencrypt_listen"), check.Equals, ":http")
c.Assert(viper.GetString("tls_letsencrypt_challenge_type"), check.Equals, "HTTP-01")
c.Assert(viper.GetString("tls.letsencrypt_hostname"), check.Equals, "")
c.Assert(viper.GetString("tls.letsencrypt_listen"), check.Equals, ":http")
c.Assert(viper.GetString("tls.letsencrypt_challenge_type"), check.Equals, "HTTP-01")
c.Assert(viper.GetStringSlice("dns_config.nameservers")[0], check.Equals, "1.1.1.1")
c.Assert(
util.GetFileMode("unix_socket_permission"),
@ -180,12 +180,12 @@ noise:
c.Assert(
tmp,
check.Matches,
".*Fatal config error: set either tls_letsencrypt_hostname or tls_cert_path/tls_key_path, not both.*",
".*Fatal config error: set either tls.letsencrypt_hostname or tls.cert_path/tls.key_path, not both.*",
)
c.Assert(
tmp,
check.Matches,
".*Fatal config error: the only supported values for tls_letsencrypt_challenge_type are.*",
".*Fatal config error: the only supported values for tls.letsencrypt_challenge_type are.*",
)
c.Assert(
tmp,
@ -193,6 +193,8 @@ noise:
".*Fatal config error: server_url must start with https:// or http://.*",
)
viper.Reset()
// Check configuration validation errors (2)
configYaml = []byte(`---
noise:

View file

@ -24,21 +24,22 @@ listen_addr: 127.0.0.1:8080
#
metrics_listen_addr: 127.0.0.1:9090
# Address to listen for gRPC.
# gRPC is used for controlling a headscale server
# remotely with the CLI
# Note: Remote access _only_ works if you have
# valid certificates.
#
# For production:
# grpc_listen_addr: 0.0.0.0:50443
grpc_listen_addr: 127.0.0.1:50443
grpc:
# Address to listen for gRPC.
# Note: Remote access _only_ works if you have
# valid certificates.
#
# For production:
# listen_addr: 0.0.0.0:50443
listen_addr: 127.0.0.1:50443
# Allow the gRPC admin interface to run in INSECURE
# mode. This is not recommended as the traffic will
# be unencrypted. Only enable if you know what you
# are doing.
grpc_allow_insecure: false
# Allow the gRPC admin interface to run in INSECURE
# mode. This is not recommended as the traffic will
# be unencrypted. Only enable if you know what you
# are doing.
allow_insecure: false
# The Noise section includes specific configuration for the
# TS2021 Noise protocol
@ -165,38 +166,44 @@ database:
# ssl: false
### TLS configuration
#
## Let's encrypt / ACME
#
# headscale supports automatically requesting and setting up
# TLS for a domain with Let's Encrypt.
#
# URL to ACME directory
acme_url: https://acme-v02.api.letsencrypt.org/directory
tls:
## Let's encrypt / ACME
#
# headscale supports automatically requesting and setting up
# TLS for a domain with Let's Encrypt.
#
# URL to ACME directory
acme_url: https://acme-v02.api.letsencrypt.org/directory
# Email to register with ACME provider
acme_email: ""
# Email to register with ACME provider
acme_email: ""
# Domain name to request a TLS certificate for:
tls_letsencrypt_hostname: ""
# Domain name to request a TLS certificate for:
letsencrypt_hostname: ""
# Path to store certificates and metadata needed by
# letsencrypt
# For production:
tls_letsencrypt_cache_dir: /var/lib/headscale/cache
# Path to store certificates and metadata needed by
# letsencrypt
# For production:
letsencrypt_cache_dir: /var/lib/headscale/cache
# Type of ACME challenge to use, currently supported types:
# HTTP-01 or TLS-ALPN-01
# See [docs/tls.md](docs/tls.md) for more information
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"
# Type of ACME challenge to use, currently supported types:
# HTTP-01 or TLS-ALPN-01
# See [docs/tls.md](docs/tls.md) for more information
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
letsencrypt_listen: ":http"
## Use already defined certificates:
tls_cert_path: ""
tls_key_path: ""
## Use already defined certificates:
cert_path: ""
key_path: ""
# Use a separate x509 certificate for gRPC, this is used
# instead of the global certificate.
grpc:
cert_path: ""
key_path: ""
log:
# Output formatting for logs: text or json

View file

@ -7,7 +7,7 @@
- Access to create API keys (local access to the `headscale` server)
- `headscale` _must_ be served over TLS/HTTPS
- Remote access does _not_ support unencrypted traffic.
- Port `50443` must be open in the firewall (or port overridden by `grpc_listen_addr` option)
- Port `50443` must be open in the firewall (or port overridden by `grpc.listen_addr` option)
## Goal

View file

@ -653,9 +653,27 @@ func (h *Headscale) Serve() error {
// https://github.com/soheilhy/cmux/issues/68
// https://github.com/soheilhy/cmux/issues/91
grpcTlsConfig := &tls.Config{
NextProtos: []string{"http/1.1"},
Certificates: make([]tls.Certificate, 1),
MinVersion: tls.VersionTLS12,
}
if h.cfg.TLS.GRPCCertPath == "" && h.cfg.TLS.GRPCKeyPath == "" {
grpcTlsConfig = tlsConfig
} else {
grpcTlsConfig.Certificates[0], err = tls.LoadX509KeyPair(h.cfg.TLS.GRPCCertPath, h.cfg.TLS.GRPCKeyPath)
if err != nil {
log.Error().Err(err).Msg("Failed to set up gRPC TLS configuration")
return err
}
}
var grpcServer *grpc.Server
var grpcListener net.Listener
if tlsConfig != nil || h.cfg.GRPCAllowInsecure {
if grpcTlsConfig != nil || h.cfg.GRPCAllowInsecure {
log.Info().Msgf("Enabling remote gRPC at %s", h.cfg.GRPCAddr)
grpcOptions := []grpc.ServerOption{
@ -668,9 +686,9 @@ func (h *Headscale) Serve() error {
),
}
if tlsConfig != nil {
if grpcTlsConfig != nil {
grpcOptions = append(grpcOptions,
grpc.Creds(credentials.NewTLS(tlsConfig)),
grpc.Creds(credentials.NewTLS(grpcTlsConfig)),
)
} else {
log.Warn().Msg("gRPC is running without security")

View file

@ -108,8 +108,10 @@ type DatabaseConfig struct {
}
type TLSConfig struct {
CertPath string
KeyPath string
CertPath string
KeyPath string
GRPCCertPath string
GRPCKeyPath string
LetsEncrypt LetsEncryptConfig
}
@ -178,6 +180,13 @@ type Tuning struct {
NodeMapSessionBufferedChanSize int
}
func RegisterDeprecatedAlias(old, new string) {
if viper.IsSet(old) {
log.Warn().Msgf("%s is deprecated and may be removed in future versions, please use %s instead.", old, new)
viper.Set(new, viper.GetString(old))
}
}
func LoadConfig(path string, isFile bool) error {
if isFile {
viper.SetConfigFile(path)
@ -197,8 +206,8 @@ func LoadConfig(path string, isFile bool) error {
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
viper.AutomaticEnv()
viper.SetDefault("tls_letsencrypt_cache_dir", "/var/www/.cache")
viper.SetDefault("tls_letsencrypt_challenge_type", HTTP01ChallengeType)
viper.SetDefault("tls.letsencrypt_cache_dir", "/var/www/.cache")
viper.SetDefault("tls.letsencrypt_challenge_type", HTTP01ChallengeType)
viper.SetDefault("log.level", "info")
viper.SetDefault("log.format", TextLogFormat)
@ -214,8 +223,8 @@ func LoadConfig(path string, isFile bool) error {
viper.SetDefault("unix_socket", "/var/run/headscale/headscale.sock")
viper.SetDefault("unix_socket_permission", "0o770")
viper.SetDefault("grpc_listen_addr", ":50443")
viper.SetDefault("grpc_allow_insecure", false)
viper.SetDefault("grpc.listen_addr", ":50443")
viper.SetDefault("grpc.allow_insecure", false)
viper.SetDefault("cli.timeout", "5s")
viper.SetDefault("cli.insecure", false)
@ -254,28 +263,39 @@ func LoadConfig(path string, isFile bool) error {
return fmt.Errorf("fatal error reading config file: %w", err)
}
RegisterDeprecatedAlias("grpc_listen_addr", "grpc.listen_addr")
RegisterDeprecatedAlias("grpc_allow_insecure", "grpc.allow_insecure")
RegisterDeprecatedAlias("acme_url", "tls.acme_url")
RegisterDeprecatedAlias("acme_email", "tls.acme_email")
RegisterDeprecatedAlias("tls_letsencrypt_hostname", "tls.letsencrypt_hostname")
RegisterDeprecatedAlias("tls_letsencrypt_cache_dir", "tls.letsencrypt_cache_dir")
RegisterDeprecatedAlias("tls_letsencrypt_challenge_type", "tls.letsencrypt_challenge_type")
RegisterDeprecatedAlias("tls_letsencrypt_listen", "tls.letsencrypt_listen")
RegisterDeprecatedAlias("tls_cert_path", "tls.cert_path")
RegisterDeprecatedAlias("tls_key_path", "tls.key_path")
// Collect any validation errors and return them all at once
var errorText string
if (viper.GetString("tls_letsencrypt_hostname") != "") &&
((viper.GetString("tls_cert_path") != "") || (viper.GetString("tls_key_path") != "")) {
errorText += "Fatal config error: set either tls_letsencrypt_hostname or tls_cert_path/tls_key_path, not both\n"
if (viper.GetString("tls.letsencrypt_hostname") != "") &&
((viper.GetString("tls.cert_path") != "") || (viper.GetString("tls.key_path") != "")) {
errorText += "Fatal config error: set either tls.letsencrypt_hostname or tls.cert_path/tls.key_path, not both\n"
}
if !viper.IsSet("noise") || viper.GetString("noise.private_key_path") == "" {
errorText += "Fatal config error: headscale now requires a new `noise.private_key_path` field in the config file for the Tailscale v2 protocol\n"
}
if (viper.GetString("tls_letsencrypt_hostname") != "") &&
(viper.GetString("tls_letsencrypt_challenge_type") == TLSALPN01ChallengeType) &&
if (viper.GetString("tls.letsencrypt_hostname") != "") &&
(viper.GetString("tls.letsencrypt_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.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")
}
if (viper.GetString("tls_letsencrypt_challenge_type") != HTTP01ChallengeType) &&
(viper.GetString("tls_letsencrypt_challenge_type") != TLSALPN01ChallengeType) {
errorText += "Fatal config error: the only supported values for tls_letsencrypt_challenge_type are HTTP-01 and TLS-ALPN-01\n"
if (viper.GetString("tls.letsencrypt_challenge_type") != HTTP01ChallengeType) &&
(viper.GetString("tls.letsencrypt_challenge_type") != TLSALPN01ChallengeType) {
errorText += "Fatal config error: the only supported values for tls.letsencrypt_challenge_type are HTTP-01 and TLS-ALPN-01\n"
}
if !strings.HasPrefix(viper.GetString("server_url"), "http://") &&
@ -305,18 +325,24 @@ func LoadConfig(path string, isFile bool) error {
func GetTLSConfig() TLSConfig {
return TLSConfig{
LetsEncrypt: LetsEncryptConfig{
Hostname: viper.GetString("tls_letsencrypt_hostname"),
Listen: viper.GetString("tls_letsencrypt_listen"),
Hostname: viper.GetString("tls.letsencrypt_hostname"),
Listen: viper.GetString("tls.letsencrypt_listen"),
CacheDir: util.AbsolutePathFromConfigPath(
viper.GetString("tls_letsencrypt_cache_dir"),
viper.GetString("tls.letsencrypt_cache_dir"),
),
ChallengeType: viper.GetString("tls_letsencrypt_challenge_type"),
ChallengeType: viper.GetString("tls.letsencrypt_challenge_type"),
},
CertPath: util.AbsolutePathFromConfigPath(
viper.GetString("tls_cert_path"),
viper.GetString("tls.cert_path"),
),
KeyPath: util.AbsolutePathFromConfigPath(
viper.GetString("tls_key_path"),
viper.GetString("tls.key_path"),
),
GRPCCertPath: util.AbsolutePathFromConfigPath(
viper.GetString("tls.grpc.cert_path"),
),
GRPCKeyPath: util.AbsolutePathFromConfigPath(
viper.GetString("tls.grpc.key_path"),
),
}
}
@ -698,8 +724,8 @@ func GetHeadscaleConfig() (*Config, error) {
ServerURL: viper.GetString("server_url"),
Addr: viper.GetString("listen_addr"),
MetricsAddr: viper.GetString("metrics_listen_addr"),
GRPCAddr: viper.GetString("grpc_listen_addr"),
GRPCAllowInsecure: viper.GetBool("grpc_allow_insecure"),
GRPCAddr: viper.GetString("grpc.listen_addr"),
GRPCAllowInsecure: viper.GetBool("grpc.allow_insecure"),
DisableUpdateCheck: viper.GetBool("disable_check_updates"),
PrefixV4: prefix4,
@ -724,8 +750,8 @@ func GetHeadscaleConfig() (*Config, error) {
DNSConfig: dnsConfig,
DNSUserNameInMagicDNS: viper.GetBool("dns_config.use_username_in_magic_dns"),
ACMEEmail: viper.GetString("acme_email"),
ACMEURL: viper.GetString("acme_url"),
ACMEEmail: viper.GetString("tls.acme_email"),
ACMEURL: viper.GetString("tls.acme_url"),
UnixSocket: viper.GetString("unix_socket"),
UnixSocketPermission: util.GetFileMode("unix_socket_permission"),

View file

@ -43,6 +43,7 @@ func TestDERPServerScenario(t *testing.T) {
hsic.WithExtraPorts([]string{"3478/udp"}),
hsic.WithEmbeddedDERPServerOnly(),
hsic.WithTLS(),
hsic.WithGrpcTLS(),
hsic.WithHostnameAsServerURL(),
)
assertNoErrHeadscaleEnv(t, err)

View file

@ -41,6 +41,7 @@ func TestPingAllByIP(t *testing.T) {
hsic.WithTestName("pingallbyip"),
hsic.WithEmbeddedDERPServerOnly(),
hsic.WithTLS(),
hsic.WithGrpcTLS(),
hsic.WithHostnameAsServerURL(),
hsic.WithIPAllocationStrategy(types.IPAllocationStrategyRandom),
)
@ -836,6 +837,7 @@ func TestPingAllByIPManyUpDown(t *testing.T) {
hsic.WithTestName("pingallbyipmany"),
hsic.WithEmbeddedDERPServerOnly(),
hsic.WithTLS(),
hsic.WithGrpcTLS(),
hsic.WithHostnameAsServerURL(),
)
assertNoErrHeadscaleEnv(t, err)

View file

@ -40,6 +40,8 @@ const (
aclPolicyPath = "/etc/headscale/acl.hujson"
tlsCertPath = "/etc/headscale/tls.cert"
tlsKeyPath = "/etc/headscale/tls.key"
grpcTlsCertPath = "/etc/headscale/grpc_tls.cert"
grpcTlsKeyPath = "/etc/headscale/grpc_tls.key"
headscaleDefaultPort = 8080
)
@ -69,6 +71,8 @@ type HeadscaleInContainer struct {
env map[string]string
tlsCert []byte
tlsKey []byte
grpcTlsCert []byte
grpcTlsKey []byte
filesInContainer []fileInContainer
postgres bool
}
@ -105,6 +109,19 @@ func WithTLS() Option {
}
}
// WithGrpcTLS creates gRPC certificates and enables them.
func WithGrpcTLS() Option {
return func(hsic *HeadscaleInContainer) {
cert, key, err := createCertificate(hsic.hostname)
if err != nil {
log.Fatalf("failed to create grpc certificates for headscale test: %s", err)
}
hsic.grpcTlsCert = cert
hsic.grpcTlsKey = key
}
}
// WithConfigEnv takes a map of environment variables that
// can be used to override Headscale configuration.
func WithConfigEnv(configEnv map[string]string) Option {
@ -253,6 +270,11 @@ func New(
hsic.env["HEADSCALE_SERVER_URL"] = serverURL.String()
}
if hsic.hasGrpcTLS() {
hsic.env["HEADSCALE_GRPC_TLS_CERT_PATH"] = grpcTlsCertPath
hsic.env["HEADSCALE_GRPC_TLS_KEY_PATH"] = grpcTlsKeyPath
}
headscaleBuildOptions := &dockertest.BuildOptions{
Dockerfile: "Dockerfile.debug",
ContextDir: dockerContextPath,
@ -374,6 +396,18 @@ func New(
}
}
if hsic.hasGrpcTLS() {
err = hsic.WriteFile(grpcTlsCertPath, hsic.grpcTlsCert)
if err != nil {
return nil, fmt.Errorf("failed to write TLS certificate to container: %w", err)
}
err = hsic.WriteFile(grpcTlsKeyPath, hsic.grpcTlsKey)
if err != nil {
return nil, fmt.Errorf("failed to write TLS key to container: %w", err)
}
}
for _, f := range hsic.filesInContainer {
if err := hsic.WriteFile(f.path, f.contents); err != nil {
return nil, fmt.Errorf("failed to write %q: %w", f.path, err)
@ -391,6 +425,10 @@ func (t *HeadscaleInContainer) hasTLS() bool {
return len(t.tlsCert) != 0 && len(t.tlsKey) != 0
}
func (t *HeadscaleInContainer) hasGrpcTLS() bool {
return len(t.grpcTlsCert) != 0 && len(t.grpcTlsKey) != 0
}
// Shutdown stops and cleans up the Headscale container.
func (t *HeadscaleInContainer) Shutdown() error {
err := t.SaveLog("/tmp/control")