mirror of
https://github.com/tailscale/tailscale.git
synced 2024-11-29 13:05:46 +00:00
4f1d6c53cb
This conforms to the NGINX subrequest result authentication protocol[1] using the NGINX module `ngx_http_auth_request_module`. This is based on the example that @peterkeen provided on Twitter[2], but with several changes to make things more tightly locked down: * This listens over a UNIX socket instead of a TCP socket to prevent leakage to the network * This uses systemd socket activation so that systemd owns the socket and can then lock down the service to the bare minimum required to do its job without having to worry about dropping permissions * This provides additional information in HTTP response headers that can be useful for integrating with various services * This has a script to automagically create debian and redhat packages for easier distribution This will be written about on the Tailscale blog. There is more information in README.md. [1]: https://docs.nginx.com/nginx/admin-guide/security-controls/configuring-subrequest-authentication/ [2]: https://github.com/peterkeen/tailscale/blob/main/cmd/nginx-auth-proxy/nginx-auth-proxy.go Signed-off-by: Xe Iaso <xe@tailscale.com>
121 lines
3.5 KiB
Go
121 lines
3.5 KiB
Go
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
|
|
// Use of this source code is governed by a BSD-style
|
|
// license that can be found in the LICENSE file.
|
|
|
|
//go:build linux
|
|
|
|
// Command nginx-auth is a tool that allows users to use Tailscale Whois
|
|
// authentication with NGINX as a reverse proxy. This allows users that
|
|
// already have a bunch of services hosted on an internal NGINX server
|
|
// to point those domains to the Tailscale IP of the NGINX server and
|
|
// then seamlessly use Tailscale for authentication.
|
|
package main
|
|
|
|
import (
|
|
"flag"
|
|
"log"
|
|
"net"
|
|
"net/http"
|
|
"net/netip"
|
|
"os"
|
|
"strings"
|
|
|
|
"github.com/coreos/go-systemd/activation"
|
|
"tailscale.com/client/tailscale"
|
|
)
|
|
|
|
var (
|
|
sockPath = flag.String("sockpath", "", "the filesystem path for the unix socket this service exposes")
|
|
)
|
|
|
|
func main() {
|
|
flag.Parse()
|
|
|
|
mux := http.NewServeMux()
|
|
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
|
remoteHost := r.Header.Get("Remote-Addr")
|
|
remotePort := r.Header.Get("Remote-Port")
|
|
if remoteHost == "" || remotePort == "" {
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
log.Println("set Remote-Addr to $remote_addr and Remote-Port to $remote_port in your nginx config")
|
|
return
|
|
}
|
|
|
|
remoteAddrStr := net.JoinHostPort(remoteHost, remotePort)
|
|
remoteAddr, err := netip.ParseAddrPort(remoteAddrStr)
|
|
if err != nil {
|
|
w.WriteHeader(http.StatusUnauthorized)
|
|
log.Printf("remote address and port are not valid: %v", err)
|
|
return
|
|
}
|
|
|
|
info, err := tailscale.WhoIs(r.Context(), remoteAddr.String())
|
|
if err != nil {
|
|
w.WriteHeader(http.StatusUnauthorized)
|
|
log.Printf("can't look up %s: %v", remoteAddr, err)
|
|
return
|
|
}
|
|
|
|
if len(info.Node.Tags) != 0 {
|
|
w.WriteHeader(http.StatusForbidden)
|
|
log.Printf("node %s is tagged", info.Node.Hostinfo.Hostname())
|
|
return
|
|
}
|
|
|
|
_, tailnet, ok := strings.Cut(info.Node.Name, info.Node.ComputedName+".")
|
|
if !ok {
|
|
w.WriteHeader(http.StatusUnauthorized)
|
|
log.Printf("can't extract tailnet name from hostname %q", info.Node.Name)
|
|
return
|
|
}
|
|
tailnet, _, ok = strings.Cut(tailnet, ".beta.tailscale.net")
|
|
if !ok {
|
|
w.WriteHeader(http.StatusUnauthorized)
|
|
log.Printf("can't extract tailnet name from hostname %q", info.Node.Name)
|
|
return
|
|
}
|
|
|
|
h := w.Header()
|
|
h.Set("Tailscale-Login", strings.Split(info.UserProfile.LoginName, "@")[0])
|
|
h.Set("Tailscale-User", info.UserProfile.LoginName)
|
|
h.Set("Tailscale-Name", info.UserProfile.DisplayName)
|
|
h.Set("Tailscale-Profile-Picture", info.UserProfile.ProfilePicURL)
|
|
h.Set("Tailscale-Tailnet", tailnet)
|
|
w.WriteHeader(http.StatusNoContent)
|
|
})
|
|
|
|
if *sockPath != "" {
|
|
_ = os.Remove(*sockPath) // ignore error, this file may not already exist
|
|
ln, err := net.Listen("unix", *sockPath)
|
|
if err != nil {
|
|
log.Fatalf("can't listen on %s: %v", *sockPath, err)
|
|
}
|
|
defer ln.Close()
|
|
|
|
log.Printf("listening on %s", *sockPath)
|
|
log.Fatal(http.Serve(ln, mux))
|
|
}
|
|
|
|
listeners, err := activation.Listeners()
|
|
if err != nil {
|
|
log.Fatalf("no sockets passed to this service with systemd: %v", err)
|
|
}
|
|
|
|
// NOTE(Xe): normally you'd want to make a waitgroup here and then register
|
|
// each listener with it. In this case I want this to blow up horribly if
|
|
// any of the listeners stop working. systemd will restart it due to the
|
|
// socket activation at play.
|
|
//
|
|
// TL;DR: Let it crash, it will come back
|
|
for _, ln := range listeners {
|
|
go func(ln net.Listener) {
|
|
log.Printf("listening on %s", ln.Addr())
|
|
log.Fatal(http.Serve(ln, mux))
|
|
}(ln)
|
|
}
|
|
|
|
for {
|
|
select {}
|
|
}
|
|
}
|