1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
module ProTacts
# Gates every request on the identity headers Tailscale serve injects.
#
# Serve sets Tailscale-User-Login from the tailnet identity of the calling
# node, and strips the header from incoming requests before proxying so a
# client cannot supply its own. That makes the header trustworthy, but only
# behind serve: reached directly, this middleware trusts whatever it is
# handed. The app must not be listening anywhere but localhost.
#
# Failing closed covers the two cases Tailscale documents as having no
# identity: Funnel traffic, which is public, and traffic from tagged
# devices. A family device that gets tagged will start seeing 403s.
#
# Any tailnet identity is accepted. Getting onto the tailnet is the access
# control; the address book has no per-user view to protect.
class TailscaleAuth
# @rbs @app: Rack::_App
LOGIN_HEADER = "HTTP_TAILSCALE_USER_LOGIN"
NAME_HEADER = "HTTP_TAILSCALE_USER_NAME"
# Where the authenticated login lands for anything downstream that wants
# to know who is asking.
IDENTITY = "pro_tacts.user"
#: (Rack::_App app) -> void
def initialize(app)
@app = app
end
#: (Rack::env env) -> Rack::response
def call(env)
login = env[LOGIN_HEADER].to_s.strip
# 403 rather than 401: no credentials the client could supply would
# help, so there is no challenge worth sending.
return forbidden if login.empty?
env[IDENTITY] = login
@app.call(env)
end
private
#: () -> Rack::response
def forbidden
body = "Forbidden: no Tailscale identity on this request.\n"
[403, { "Content-Type" => "text/plain", "Content-Length" => body.bytesize.to_s }, [body]]
end
end
end