nginx-auth.go 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128
  1. // Copyright (c) Tailscale Inc & AUTHORS
  2. // SPDX-License-Identifier: BSD-3-Clause
  3. //go:build linux
  4. // Command nginx-auth is a tool that allows users to use Tailscale Whois
  5. // authentication with NGINX as a reverse proxy. This allows users that
  6. // already have a bunch of services hosted on an internal NGINX server
  7. // to point those domains to the Tailscale IP of the NGINX server and
  8. // then seamlessly use Tailscale for authentication.
  9. package main
  10. import (
  11. "flag"
  12. "log"
  13. "net"
  14. "net/http"
  15. "net/netip"
  16. "net/url"
  17. "os"
  18. "strings"
  19. "github.com/coreos/go-systemd/activation"
  20. "tailscale.com/client/tailscale"
  21. )
  22. var (
  23. sockPath = flag.String("sockpath", "", "the filesystem path for the unix socket this service exposes")
  24. )
  25. func main() {
  26. flag.Parse()
  27. mux := http.NewServeMux()
  28. mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
  29. remoteHost := r.Header.Get("Remote-Addr")
  30. remotePort := r.Header.Get("Remote-Port")
  31. if remoteHost == "" || remotePort == "" {
  32. w.WriteHeader(http.StatusBadRequest)
  33. log.Println("set Remote-Addr to $remote_addr and Remote-Port to $remote_port in your nginx config")
  34. return
  35. }
  36. remoteAddrStr := net.JoinHostPort(remoteHost, remotePort)
  37. remoteAddr, err := netip.ParseAddrPort(remoteAddrStr)
  38. if err != nil {
  39. w.WriteHeader(http.StatusUnauthorized)
  40. log.Printf("remote address and port are not valid: %v", err)
  41. return
  42. }
  43. info, err := tailscale.WhoIs(r.Context(), remoteAddr.String())
  44. if err != nil {
  45. w.WriteHeader(http.StatusUnauthorized)
  46. log.Printf("can't look up %s: %v", remoteAddr, err)
  47. return
  48. }
  49. if info.Node.IsTagged() {
  50. w.WriteHeader(http.StatusForbidden)
  51. log.Printf("node %s is tagged", info.Node.Hostinfo.Hostname())
  52. return
  53. }
  54. // tailnet of connected node. When accessing shared nodes, this
  55. // will be empty because the tailnet of the sharee is not exposed.
  56. var tailnet string
  57. if !info.Node.Hostinfo.ShareeNode() {
  58. var ok bool
  59. _, tailnet, ok = strings.Cut(info.Node.Name, info.Node.ComputedName+".")
  60. if !ok {
  61. w.WriteHeader(http.StatusUnauthorized)
  62. log.Printf("can't extract tailnet name from hostname %q", info.Node.Name)
  63. return
  64. }
  65. tailnet = strings.TrimSuffix(tailnet, ".beta.tailscale.net")
  66. }
  67. if expectedTailnet := r.Header.Get("Expected-Tailnet"); expectedTailnet != "" && expectedTailnet != tailnet {
  68. w.WriteHeader(http.StatusForbidden)
  69. log.Printf("user is part of tailnet %s, wanted: %s", tailnet, url.QueryEscape(expectedTailnet))
  70. return
  71. }
  72. h := w.Header()
  73. h.Set("Tailscale-Login", strings.Split(info.UserProfile.LoginName, "@")[0])
  74. h.Set("Tailscale-User", info.UserProfile.LoginName)
  75. h.Set("Tailscale-Name", info.UserProfile.DisplayName)
  76. h.Set("Tailscale-Profile-Picture", info.UserProfile.ProfilePicURL)
  77. h.Set("Tailscale-Tailnet", tailnet)
  78. w.WriteHeader(http.StatusNoContent)
  79. })
  80. if *sockPath != "" {
  81. _ = os.Remove(*sockPath) // ignore error, this file may not already exist
  82. ln, err := net.Listen("unix", *sockPath)
  83. if err != nil {
  84. log.Fatalf("can't listen on %s: %v", *sockPath, err)
  85. }
  86. defer ln.Close()
  87. log.Printf("listening on %s", *sockPath)
  88. log.Fatal(http.Serve(ln, mux))
  89. }
  90. listeners, err := activation.Listeners()
  91. if err != nil {
  92. log.Fatalf("no sockets passed to this service with systemd: %v", err)
  93. }
  94. // NOTE(Xe): normally you'd want to make a waitgroup here and then register
  95. // each listener with it. In this case I want this to blow up horribly if
  96. // any of the listeners stop working. systemd will restart it due to the
  97. // socket activation at play.
  98. //
  99. // TL;DR: Let it crash, it will come back
  100. for _, ln := range listeners {
  101. go func(ln net.Listener) {
  102. log.Printf("listening on %s", ln.Addr())
  103. log.Fatal(http.Serve(ln, mux))
  104. }(ln)
  105. }
  106. for {
  107. select {}
  108. }
  109. }