mirror of https://github.com/tailscale/tailscale/
cmd/proxy-to-grafana: use grafana's authproxy to log in tailnet users (#4208)
Signed-off-by: Nick O'Neill <nick@tailscale.com>pull/4266/head
parent
4fc38888d2
commit
5fa502b5dc
@ -0,0 +1,154 @@
|
||||
// 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.
|
||||
|
||||
// proxy-to-grafana is a reverse proxy which identifies users based on their
|
||||
// originating Tailscale identity and maps them to corresponding Grafana
|
||||
// users, creating them if needed.
|
||||
//
|
||||
// It uses Grafana's AuthProxy feature:
|
||||
// https://grafana.com/docs/grafana/latest/auth/auth-proxy/
|
||||
//
|
||||
// Set the TS_AUTHKEY environment variable to have this server automatically
|
||||
// join your tailnet, or look for the logged auth link on first start.
|
||||
//
|
||||
// Use this Grafana configuration to enable the auth proxy:
|
||||
//
|
||||
// ```
|
||||
// [auth.proxy]
|
||||
// enabled = true
|
||||
// header_name = X-WEBAUTH-USER
|
||||
// header_property = username
|
||||
// auto_sign_up = true
|
||||
// whitelist = 127.0.0.1
|
||||
// headers = Name:X-WEBAUTH-NAME
|
||||
// enable_login_token = true
|
||||
// ```
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"tailscale.com/client/tailscale"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/tsnet"
|
||||
)
|
||||
|
||||
var (
|
||||
hostname = flag.String("hostname", "", "Tailscale hostname to serve on, used as the base name for MagicDNS or subdomain in your domain alias for HTTPS.")
|
||||
backendAddr = flag.String("backend-addr", "", "Address of the Grafana server served over HTTP, in host:port format. Typically localhost:nnnn.")
|
||||
tailscaleDir = flag.String("state-dir", "./", "Alternate directory to use for Tailscale state storage. If empty, a default is used.")
|
||||
useHTTPS = flag.Bool("use-https", false, "Serve over HTTPS via your *.ts.net subdomain if enabled in Tailscale admin.")
|
||||
)
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
if *hostname == "" || strings.Contains(*hostname, ".") {
|
||||
log.Fatal("missing or invalid --hostname")
|
||||
}
|
||||
if *backendAddr == "" {
|
||||
log.Fatal("missing --backend-addr")
|
||||
}
|
||||
ts := &tsnet.Server{
|
||||
Dir: *tailscaleDir,
|
||||
Hostname: *hostname,
|
||||
}
|
||||
|
||||
url, err := url.Parse(fmt.Sprintf("http://%s", *backendAddr))
|
||||
if err != nil {
|
||||
log.Fatalf("couldn't parse backend address: %v", err)
|
||||
}
|
||||
|
||||
proxy := httputil.NewSingleHostReverseProxy(url)
|
||||
originalDirector := proxy.Director
|
||||
proxy.Director = func(req *http.Request) {
|
||||
originalDirector(req)
|
||||
modifyRequest(req)
|
||||
}
|
||||
|
||||
var ln net.Listener
|
||||
if *useHTTPS {
|
||||
ln, err = ts.Listen("tcp", ":443")
|
||||
ln = tls.NewListener(ln, &tls.Config{
|
||||
GetCertificate: tailscale.GetCertificate,
|
||||
})
|
||||
|
||||
go func() {
|
||||
// wait for tailscale to start before trying to fetch cert names
|
||||
for i := 0; i < 60; i++ {
|
||||
st, err := tailscale.Status(context.Background())
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
log.Printf("tailscale status: %v", st.BackendState)
|
||||
if st.BackendState == "Running" {
|
||||
break
|
||||
}
|
||||
time.Sleep(time.Second)
|
||||
}
|
||||
|
||||
l80, err := ts.Listen("tcp", ":80")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
name, ok := tailscale.ExpandSNIName(context.Background(), *hostname)
|
||||
if !ok {
|
||||
log.Fatalf("can't get hostname for https redirect")
|
||||
}
|
||||
if err := http.Serve(l80, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
http.Redirect(w, r, fmt.Sprintf("https://%s", name), http.StatusMovedPermanently)
|
||||
})); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}()
|
||||
} else {
|
||||
ln, err = ts.Listen("tcp", ":80")
|
||||
}
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
log.Printf("proxy-to-grafana running at %v, proxying to %v", ln.Addr(), *backendAddr)
|
||||
log.Fatal(http.Serve(ln, proxy))
|
||||
}
|
||||
|
||||
func modifyRequest(req *http.Request) {
|
||||
// with enable_login_token set to true, we get a cookie that handles
|
||||
// auth for paths that are not /login
|
||||
if req.URL.Path != "/login" {
|
||||
return
|
||||
}
|
||||
|
||||
user, err := getTailscaleUser(req.Context(), req.RemoteAddr)
|
||||
if err != nil {
|
||||
log.Printf("error getting Tailscale user: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
req.Header.Set("X-Webauth-User", user.LoginName)
|
||||
req.Header.Set("X-Webauth-Name", user.DisplayName)
|
||||
}
|
||||
|
||||
func getTailscaleUser(ctx context.Context, ipPort string) (*tailcfg.UserProfile, error) {
|
||||
whois, err := tailscale.WhoIs(ctx, ipPort)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to identify remote host: %w", err)
|
||||
}
|
||||
if len(whois.Node.Tags) != 0 {
|
||||
return nil, fmt.Errorf("tagged nodes are not users")
|
||||
}
|
||||
if whois.UserProfile == nil || whois.UserProfile.LoginName == "" {
|
||||
return nil, fmt.Errorf("failed to identify remote user")
|
||||
}
|
||||
|
||||
return whois.UserProfile, nil
|
||||
}
|
Loading…
Reference in New Issue