app: simplify TLS and auth setup

This commit is contained in:
Kristoffer Dalby
2026-06-16 12:25:37 +00:00
parent 19d5d9deeb
commit 62869f01d2
+50 -40
View File
@@ -204,13 +204,15 @@ func NewHeadscale(cfg *types.Config) (*Headscale, error) {
if cfg.PrefixV4 != nil { if cfg.PrefixV4 != nil {
magicDNSDomains = append( magicDNSDomains = append(
magicDNSDomains, magicDNSDomains,
util.GenerateIPv4DNSRootDomain(*cfg.PrefixV4)...) util.GenerateIPv4DNSRootDomain(*cfg.PrefixV4)...,
)
} }
if cfg.PrefixV6 != nil { if cfg.PrefixV6 != nil {
magicDNSDomains = append( magicDNSDomains = append(
magicDNSDomains, magicDNSDomains,
util.GenerateIPv6DNSRootDomain(*cfg.PrefixV6)...) util.GenerateIPv6DNSRootDomain(*cfg.PrefixV6)...,
)
} }
// we might have routes already from Split DNS // we might have routes already from Split DNS
@@ -283,7 +285,7 @@ func (h *Headscale) scheduledTasks(ctx context.Context) {
lastExpiryCheck := time.Unix(0, 0) lastExpiryCheck := time.Unix(0, 0)
derpTickerChan := make(<-chan time.Time) var derpTickerChan <-chan time.Time
if h.cfg.DERP.AutoUpdate && h.cfg.DERP.UpdateFrequency != 0 { if h.cfg.DERP.AutoUpdate && h.cfg.DERP.UpdateFrequency != 0 {
derpTicker := time.NewTicker(h.cfg.DERP.UpdateFrequency) derpTicker := time.NewTicker(h.cfg.DERP.UpdateFrequency)
@@ -295,8 +297,6 @@ func (h *Headscale) scheduledTasks(ctx context.Context) {
var extraRecordsUpdate <-chan []tailcfg.DNSRecord var extraRecordsUpdate <-chan []tailcfg.DNSRecord
if h.extraRecordMan != nil { if h.extraRecordMan != nil {
extraRecordsUpdate = h.extraRecordMan.UpdateCh() extraRecordsUpdate = h.extraRecordMan.UpdateCh()
} else {
extraRecordsUpdate = make(chan []tailcfg.DNSRecord)
} }
var ( var (
@@ -320,8 +320,6 @@ func (h *Headscale) scheduledTasks(ctx context.Context) {
Dur("interval", h.cfg.Node.Routes.HA.ProbeInterval). Dur("interval", h.cfg.Node.Routes.HA.ProbeInterval).
Dur("timeout", h.cfg.Node.Routes.HA.ProbeTimeout). Dur("timeout", h.cfg.Node.Routes.HA.ProbeTimeout).
Msg("HA subnet router health probing enabled") Msg("HA subnet router health probing enabled")
} else {
haHealthChan = make(<-chan time.Time)
} }
for { for {
@@ -387,6 +385,20 @@ func (h *Headscale) scheduledTasks(ctx context.Context) {
} }
} }
// checkBearerToken validates an "Authorization" header value. It reports
// whether the API key is valid, whether the header carried the "Bearer "
// prefix, and any validation error. Callers translate these outcomes into
// their transport-specific status.
func (h *Headscale) checkBearerToken(authHeader string) (bool, bool, error) {
if !strings.HasPrefix(authHeader, AuthPrefix) {
return false, false, nil
}
valid, err := h.state.ValidateAPIKey(strings.TrimPrefix(authHeader, AuthPrefix))
return valid, true, err
}
func (h *Headscale) grpcAuthenticationInterceptor(ctx context.Context, func (h *Headscale) grpcAuthenticationInterceptor(ctx context.Context,
req any, req any,
info *grpc.UnaryServerInfo, info *grpc.UnaryServerInfo,
@@ -420,16 +432,14 @@ func (h *Headscale) grpcAuthenticationInterceptor(ctx context.Context,
) )
} }
token := authHeader[0] valid, hasPrefix, err := h.checkBearerToken(authHeader[0])
if !hasPrefix {
if !strings.HasPrefix(token, AuthPrefix) {
return ctx, status.Error( return ctx, status.Error(
codes.Unauthenticated, codes.Unauthenticated,
`missing "Bearer " prefix in "Authorization" header`, `missing "Bearer " prefix in "Authorization" header`,
) )
} }
valid, err := h.state.ValidateAPIKey(strings.TrimPrefix(token, AuthPrefix))
if err != nil { if err != nil {
return ctx, status.Error(codes.Internal, "validating token") return ctx, status.Error(codes.Internal, "validating token")
} }
@@ -465,7 +475,8 @@ func (h *Headscale) httpAuthenticationMiddleware(next http.Handler) http.Handler
} }
} }
if !strings.HasPrefix(authHeader, AuthPrefix) { valid, hasPrefix, err := h.checkBearerToken(authHeader)
if !hasPrefix {
log.Error(). log.Error().
Caller(). Caller().
Str("client_address", req.RemoteAddr). Str("client_address", req.RemoteAddr).
@@ -475,7 +486,6 @@ func (h *Headscale) httpAuthenticationMiddleware(next http.Handler) http.Handler
return return
} }
valid, err := h.state.ValidateAPIKey(strings.TrimPrefix(authHeader, AuthPrefix))
if err != nil { if err != nil {
log.Info(). log.Info().
Caller(). Caller().
@@ -792,7 +802,8 @@ func (h *Headscale) Serve() error {
} }
if tlsConfig != nil { if tlsConfig != nil {
grpcOptions = append(grpcOptions, grpcOptions = append(
grpcOptions,
grpc.Creds(credentials.NewTLS(tlsConfig)), grpc.Creds(credentials.NewTLS(tlsConfig)),
) )
} else { } else {
@@ -1015,14 +1026,14 @@ func (h *Headscale) Serve() error {
} }
func (h *Headscale) getTLSSettings() (*tls.Config, error) { func (h *Headscale) getTLSSettings() (*tls.Config, error) {
var err error tlsEnabled := h.cfg.TLS.LetsEncrypt.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.LetsEncrypt.Hostname != "" {
if !strings.HasPrefix(h.cfg.ServerURL, "https://") {
log.Warn().
Msg("Listening with TLS but ServerURL does not start with https://")
}
certManager := autocert.Manager{ certManager := autocert.Manager{
Prompt: autocert.AcceptTOS, Prompt: autocert.AcceptTOS,
HostPolicy: autocert.HostWhitelist(h.cfg.TLS.LetsEncrypt.Hostname), HostPolicy: autocert.HostWhitelist(h.cfg.TLS.LetsEncrypt.Hostname),
@@ -1068,27 +1079,26 @@ func (h *Headscale) getTLSSettings() (*tls.Config, error) {
default: default:
return nil, errUnsupportedLetsEncryptChallengeType return nil, errUnsupportedLetsEncryptChallengeType
} }
} else if h.cfg.TLS.CertPath == "" {
if !strings.HasPrefix(h.cfg.ServerURL, "http://") {
log.Warn().Msg("listening without TLS but ServerURL does not start with http://")
}
return nil, err
} else {
if !strings.HasPrefix(h.cfg.ServerURL, "https://") {
log.Warn().Msg("listening with TLS but ServerURL does not start with https://")
}
tlsConfig := &tls.Config{
NextProtos: []string{"http/1.1"},
Certificates: make([]tls.Certificate, 1),
MinVersion: tls.VersionTLS12,
}
tlsConfig.Certificates[0], err = tls.LoadX509KeyPair(h.cfg.TLS.CertPath, h.cfg.TLS.KeyPath)
return tlsConfig, err
} }
if h.cfg.TLS.CertPath == "" {
return nil, nil //nolint:nilnil // intentional: no TLS config when neither LetsEncrypt nor a cert path is set
}
tlsConfig := &tls.Config{
NextProtos: []string{"http/1.1"},
Certificates: make([]tls.Certificate, 1),
MinVersion: tls.VersionTLS12,
}
cert, err := tls.LoadX509KeyPair(h.cfg.TLS.CertPath, h.cfg.TLS.KeyPath)
if err != nil {
return nil, err
}
tlsConfig.Certificates[0] = cert
return tlsConfig, nil
} }
func readOrCreatePrivateKey(path string) (*key.MachinePrivate, error) { func readOrCreatePrivateKey(path string) (*key.MachinePrivate, error) {