Switching MachineKey for NodeKey wherever possible as Node identifier

This commit is contained in:
Juan Font Alonso 2022-03-29 16:54:31 +02:00
parent fc181333e5
commit 5082975289
7 changed files with 156 additions and 57 deletions

42
api.go
View file

@ -202,7 +202,7 @@ func (h *Headscale) RegistrationHandler(ctx *gin.Context) {
}
h.registrationCache.Set(
machineKeyStr,
NodePublicKeyStripPrefix(req.NodeKey),
newMachine,
registerCacheExpiration,
)
@ -477,6 +477,7 @@ func (h *Headscale) handleMachineValidRegistration(
return
}
machineRegistrations.WithLabelValues("update", "web", "success", machine.Namespace.Name).
Inc()
ctx.Data(http.StatusOK, "application/json; charset=utf-8", respBody)
@ -503,10 +504,10 @@ func (h *Headscale) handleMachineExpired(
if h.cfg.OIDC.Issuer != "" {
resp.AuthURL = fmt.Sprintf("%s/oidc/register/%s",
strings.TrimSuffix(h.cfg.ServerURL, "/"), machineKey.String())
strings.TrimSuffix(h.cfg.ServerURL, "/"), machine.NodeKey)
} else {
resp.AuthURL = fmt.Sprintf("%s/register?key=%s",
strings.TrimSuffix(h.cfg.ServerURL, "/"), machineKey.String())
strings.TrimSuffix(h.cfg.ServerURL, "/"), machine.NodeKey)
}
respBody, err := encode(resp, &machineKey, h.privateKey)
@ -521,6 +522,7 @@ func (h *Headscale) handleMachineExpired(
return
}
machineRegistrations.WithLabelValues("reauth", "web", "success", machine.Namespace.Name).
Inc()
ctx.Data(http.StatusOK, "application/json; charset=utf-8", respBody)
@ -570,13 +572,21 @@ func (h *Headscale) handleMachineRegistrationNew(
resp.AuthURL = fmt.Sprintf(
"%s/oidc/register/%s",
strings.TrimSuffix(h.cfg.ServerURL, "/"),
machineKey.String(),
NodePublicKeyStripPrefix(registerRequest.NodeKey),
)
} else {
resp.AuthURL = fmt.Sprintf("%s/register?key=%s",
strings.TrimSuffix(h.cfg.ServerURL, "/"), MachinePublicKeyStripPrefix(machineKey))
strings.TrimSuffix(h.cfg.ServerURL, "/"), NodePublicKeyStripPrefix(registerRequest.NodeKey))
}
if machineKey.IsZero() {
// TS2021
ctx.JSON(http.StatusOK, resp)
return
}
// The Tailscale legacy protocol requires to encrypt the NaCl box with the MachineKey
respBody, err := encode(resp, &machineKey, h.privateKey)
if err != nil {
log.Error().
@ -590,21 +600,15 @@ func (h *Headscale) handleMachineRegistrationNew(
ctx.Data(http.StatusOK, "application/json; charset=utf-8", respBody)
}
// TODO: check if any locks are needed around IP allocation.
func (h *Headscale) handleAuthKey(
ctx *gin.Context,
machineKey key.MachinePublic,
registerRequest tailcfg.RegisterRequest,
) {
var machineKeyStr string
if machineKey.IsZero() {
// We are handling here a Noise auth key
machineKeyStr = ""
} else {
machineKeyStr = MachinePublicKeyStripPrefix(machineKey)
}
machineKeyStr := MachinePublicKeyStripPrefix(machineKey)
log.Debug().
Caller().
Str("func", "handleAuthKey").
Str("machine", registerRequest.Hostinfo.Hostname).
Msgf("Processing auth key for %s", registerRequest.Hostinfo.Hostname)
resp := tailcfg.RegisterResponse{}
@ -651,7 +655,7 @@ func (h *Headscale) handleAuthKey(
}
log.Debug().
Caller().
Str("func", "handleAuthKey").
Str("machine", registerRequest.Hostinfo.Hostname).
Msg("Authentication key was valid, proceeding to acquire IP addresses")
@ -707,14 +711,6 @@ func (h *Headscale) handleAuthKey(
resp.MachineAuthorized = true
resp.User = *pak.Namespace.toUser()
// TS2021
if machineKey.IsZero() {
ctx.JSON(http.StatusOK, resp)
return
}
respBody, err := encode(resp, &machineKey, h.privateKey)
if err != nil {
log.Error().

2
app.go
View file

@ -484,7 +484,7 @@ func (h *Headscale) createRouter(grpcMux *runtime.ServeMux) *gin.Engine {
router.GET("/register", h.RegisterWebAPI)
router.POST("/machine/:id/map", h.PollNetMapHandler)
router.POST("/machine/:id", h.RegistrationHandler)
router.GET("/oidc/register/:mkey", h.RegisterOIDC)
router.GET("/oidc/register/:nkey", h.RegisterOIDC)
router.GET("/oidc/callback", h.OIDCCallback)
router.GET("/apple", h.AppleConfigMessage)
router.GET("/apple/:platform", h.ApplePlatformConfig)

View file

@ -5,9 +5,10 @@ import (
"context"
"time"
"github.com/juanfont/headscale/gen/go/headscale/v1"
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
"github.com/rs/zerolog/log"
"tailscale.com/tailcfg"
"tailscale.com/types/key"
)
type headscaleV1APIServer struct { // v1.HeadscaleServiceServer
@ -373,6 +374,7 @@ func (api headscaleV1APIServer) DebugCreateMachine(
MachineKey: request.GetKey(),
Name: request.GetName(),
Namespace: *namespace,
NodeKey: key.NewNode().Public().String(),
Expiry: &time.Time{},
LastSeen: &time.Time{},
@ -382,7 +384,7 @@ func (api headscaleV1APIServer) DebugCreateMachine(
}
api.h.registrationCache.Set(
request.GetKey(),
newMachine.NodeKey,
newMachine,
registerCacheExpiration,
)

View file

@ -658,11 +658,11 @@ func (machine *Machine) toProto() *v1.Machine {
}
func (h *Headscale) RegisterMachineFromAuthCallback(
machineKeyStr string,
nodeKeyStr string,
namespaceName string,
registrationMethod string,
) (*Machine, error) {
if machineInterface, ok := h.registrationCache.Get(machineKeyStr); ok {
if machineInterface, ok := h.registrationCache.Get(nodeKeyStr); ok {
if registrationMachine, ok := machineInterface.(Machine); ok {
namespace, err := h.GetNamespace(namespaceName)
if err != nil {

View file

@ -42,7 +42,7 @@ func (h *Headscale) NoiseRegistrationHandler(ctx *gin.Context) {
// If the machine has AuthKey set, handle registration via PreAuthKeys
if req.Auth.AuthKey != "" {
h.handleAuthKey(ctx, key.MachinePublic{}, req)
h.handleNoiseAuthKey(ctx, req)
return
}
@ -244,13 +244,13 @@ func (h *Headscale) NoisePollNetMapHandler(ctx *gin.Context) {
Bool("readOnly", req.ReadOnly).
Bool("omitPeers", req.OmitPeers).
Bool("stream", req.Stream).
Msg("Client map request processed")
Msg("Noise client map request processed")
if req.ReadOnly {
log.Info().
Caller().
Str("machine", machine.Name).
Msg("Client is starting up. Probably interested in a DERP map")
Msg("Noise client is starting up. Probably interested in a DERP map")
// log.Info().Str("machine", machine.Name).Bytes("resp", data).Msg("Sending DERP map to client")
ctx.Data(http.StatusOK, "application/json; charset=utf-8", data)
@ -270,7 +270,7 @@ func (h *Headscale) NoisePollNetMapHandler(ctx *gin.Context) {
Caller().
Str("id", ctx.Param("id")).
Str("machine", machine.Name).
Msg("Loading or creating update channel")
Msg("Noise loading or creating update channel")
// TODO: could probably remove all that duplication once generics land.
closeChanWithLog := func(channel interface{}, name string) {
@ -303,7 +303,7 @@ func (h *Headscale) NoisePollNetMapHandler(ctx *gin.Context) {
log.Info().
Caller().
Str("machine", machine.Name).
Msg("Client sent endpoint update and is ok with a response without peer list")
Msg("Noise client sent endpoint update and is ok with a response without peer list")
ctx.Data(http.StatusOK, "application/json; charset=utf-8", data)
// It sounds like we should update the nodes when we have received a endpoint update
@ -326,7 +326,7 @@ func (h *Headscale) NoisePollNetMapHandler(ctx *gin.Context) {
log.Info().
Caller().
Str("machine", machine.Name).
Msg("Client is ready to access the tailnet")
Msg("Noise client is ready to access the tailnet")
log.Info().
Caller().
Str("machine", machine.Name).
@ -423,11 +423,12 @@ func (h *Headscale) handleNoiseNodeExpired(
// The client has registered before, but has expired
log.Debug().
Caller().
Str("machine", machine.Name).
Msg("Machine registration has expired. Sending a authurl to register")
if registerRequest.Auth.AuthKey != "" {
h.handleAuthKey(ctx, key.MachinePublic{}, registerRequest)
h.handleNoiseAuthKey(ctx, registerRequest)
return
}
@ -444,3 +445,106 @@ func (h *Headscale) handleNoiseNodeExpired(
Inc()
ctx.JSON(http.StatusOK, resp)
}
func (h *Headscale) handleNoiseAuthKey(
ctx *gin.Context,
registerRequest tailcfg.RegisterRequest,
) {
log.Debug().
Caller().
Str("machine", registerRequest.Hostinfo.Hostname).
Msgf("Processing auth key for %s over Noise", registerRequest.Hostinfo.Hostname)
resp := tailcfg.RegisterResponse{}
pak, err := h.checkKeyValidity(registerRequest.Auth.AuthKey)
if err != nil {
log.Error().
Caller().
Str("machine", registerRequest.Hostinfo.Hostname).
Err(err).
Msg("Failed authentication via AuthKey")
resp.MachineAuthorized = false
ctx.JSON(http.StatusUnauthorized, resp)
log.Error().
Caller().
Str("machine", registerRequest.Hostinfo.Hostname).
Msg("Failed authentication via AuthKey over Noise")
if pak != nil {
machineRegistrations.WithLabelValues("new", RegisterMethodAuthKey, "error", pak.Namespace.Name).
Inc()
} else {
machineRegistrations.WithLabelValues("new", RegisterMethodAuthKey, "error", "unknown").Inc()
}
return
}
log.Debug().
Caller().
Str("machine", registerRequest.Hostinfo.Hostname).
Msg("Authentication key was valid, proceeding to acquire IP addresses")
nodeKey := NodePublicKeyStripPrefix(registerRequest.NodeKey)
// retrieve machine information if it exist
// The error is not important, because if it does not
// exist, then this is a new machine and we will move
// on to registration.
machine, _ := h.GetMachineByNodeKeys(registerRequest.NodeKey, registerRequest.OldNodeKey)
if machine != nil {
log.Trace().
Caller().
Str("machine", machine.Name).
Msg("machine already registered, refreshing with new auth key")
machine.NodeKey = nodeKey
machine.AuthKeyID = uint(pak.ID)
h.RefreshMachine(machine, registerRequest.Expiry)
} else {
now := time.Now().UTC()
machineToRegister := Machine{
Name: registerRequest.Hostinfo.Hostname,
NamespaceID: pak.Namespace.ID,
MachineKey: "",
RegisterMethod: RegisterMethodAuthKey,
Expiry: &registerRequest.Expiry,
NodeKey: nodeKey,
LastSeen: &now,
AuthKeyID: uint(pak.ID),
}
machine, err = h.RegisterMachine(
machineToRegister,
)
if err != nil {
log.Error().
Caller().
Err(err).
Msg("could not register machine")
machineRegistrations.WithLabelValues("new", RegisterMethodAuthKey, "error", pak.Namespace.Name).
Inc()
ctx.String(
http.StatusInternalServerError,
"could not register machine",
)
return
}
}
h.UsePreAuthKey(pak)
resp.MachineAuthorized = true
resp.User = *pak.Namespace.toUser()
machineRegistrations.WithLabelValues("new", RegisterMethodAuthKey, "success", pak.Namespace.Name).
Inc()
ctx.JSON(http.StatusOK, resp)
log.Info().
Caller().
Str("machine", registerRequest.Hostinfo.Hostname).
Str("ips", strings.Join(machine.IPAddresses.ToStringSlice(), ", ")).
Msg("Successfully authenticated via AuthKey on Noise")
}

36
oidc.go
View file

@ -62,10 +62,10 @@ func (h *Headscale) initOIDC() error {
// RegisterOIDC redirects to the OIDC provider for authentication
// Puts machine key in cache so the callback can retrieve it using the oidc state param
// Listens in /oidc/register/:mKey.
// Listens in /oidc/register/:nKey.
func (h *Headscale) RegisterOIDC(ctx *gin.Context) {
machineKeyStr := ctx.Param("mkey")
if machineKeyStr == "" {
nodeKeyStr := ctx.Param("nkey")
if nodeKeyStr == "" {
ctx.String(http.StatusBadRequest, "Wrong params")
return
@ -73,7 +73,7 @@ func (h *Headscale) RegisterOIDC(ctx *gin.Context) {
log.Trace().
Caller().
Str("machine_key", machineKeyStr).
Str("node_key", nodeKeyStr).
Msg("Received oidc register call")
randomBlob := make([]byte, randomByteSize)
@ -89,7 +89,7 @@ func (h *Headscale) RegisterOIDC(ctx *gin.Context) {
stateStr := hex.EncodeToString(randomBlob)[:32]
// place the machine key into the state cache, so it can be retrieved later
h.registrationCache.Set(stateStr, machineKeyStr, registerCacheExpiration)
h.registrationCache.Set(stateStr, nodeKeyStr, registerCacheExpiration)
authURL := h.oauth2Config.AuthCodeURL(stateStr)
log.Debug().Msgf("Redirecting to %s for authentication", authURL)
@ -114,7 +114,7 @@ var oidcCallbackTemplate = template.Must(
)
// OIDCCallback handles the callback from the OIDC endpoint
// Retrieves the mkey from the state cache and adds the machine to the users email namespace
// Retrieves the nkey from the state cache and adds the machine to the users email namespace
// TODO: A confirmation page for new machines should be added to avoid phishing vulnerabilities
// TODO: Add groups information from OIDC tokens into machine HostInfo
// Listens in /oidc/callback.
@ -188,32 +188,32 @@ func (h *Headscale) OIDCCallback(ctx *gin.Context) {
}
// retrieve machinekey from state cache
machineKeyIf, machineKeyFound := h.registrationCache.Get(state)
nodeKeyIf, machineKeyFound := h.registrationCache.Get(state)
if !machineKeyFound {
log.Error().
Msg("requested machine state key expired before authorisation completed")
Msg("requested node state key expired before authorisation completed")
ctx.String(http.StatusBadRequest, "state has expired")
return
}
machineKeyFromCache, machineKeyOK := machineKeyIf.(string)
nodeKeyFromCache, nodeKeyOK := nodeKeyIf.(string)
var machineKey key.MachinePublic
err = machineKey.UnmarshalText(
[]byte(MachinePublicKeyEnsurePrefix(machineKeyFromCache)),
var nodeKey key.NodePublic
err = nodeKey.UnmarshalText(
[]byte(NodePublicKeyEnsurePrefix(nodeKeyFromCache)),
)
if err != nil {
log.Error().
Msg("could not parse machine public key")
Msg("could not parse node public key")
ctx.String(http.StatusBadRequest, "could not parse public key")
return
}
if !machineKeyOK {
log.Error().Msg("could not get machine key from cache")
if !nodeKeyOK {
log.Error().Msg("could not get node key from cache")
ctx.String(
http.StatusInternalServerError,
"could not get machine key from cache",
@ -226,7 +226,7 @@ func (h *Headscale) OIDCCallback(ctx *gin.Context) {
// The error is not important, because if it does not
// exist, then this is a new machine and we will move
// on to registration.
machine, _ := h.GetMachineByMachineKey(machineKey)
machine, _ := h.GetMachineByNodeKeys(nodeKey, key.NodePublic{})
if machine != nil {
log.Trace().
@ -305,10 +305,10 @@ func (h *Headscale) OIDCCallback(ctx *gin.Context) {
return
}
machineKeyStr := MachinePublicKeyStripPrefix(machineKey)
nodeKeyStr := NodePublicKeyStripPrefix(nodeKey)
_, err = h.RegisterMachineFromAuthCallback(
machineKeyStr,
nodeKeyStr,
namespace.Name,
RegisterMethodOIDC,
)

View file

@ -388,10 +388,7 @@ func (h *Headscale) PollNetMapStream(
Str("channel", "keepAlive").
Int("bytes", len(data)).
Msg("Keep alive sent successfully")
// TODO(kradalby): Abstract away all the database calls, this can cause race conditions
// when an outdated machine object is kept alive, e.g. db is update from
// command line, but then overwritten.
err = h.UpdateMachine(machine)
// TODO(kradalbCne(machine)
if err != nil {
log.Error().
Str("handler", "PollNetMapStream").