The tscap plugin is a Goa plugin
that provides declarative authorization using Tailscale app capabilities.
Inspired by the implementation of arnz.
- Tailscale 1.92+ (app capabilities feature)
- Service must be served via
tailscale serve --accept-app-caps
To enable the plugin and make use of the tscap DSL simply import both the tscap
and the dsl packages as follows:
import (
. "goa.design/goa/v3/dsl"
tscap "goa.design/plugins/v3/tscap/dsl"
)tailscale serve --accept-app-caps example.com/cap/myapp https+insecure://localhost:8080Configure grants in your tailnet policy:
{
"grants": [
{
"src": ["group:developers"],
"dst": ["tag:myapp"],
"app": {
"example.com/cap/myapp": [{"action": ["*"], "resources": ["*"]}]
}
},
{
"src": ["group:finance"],
"dst": ["tag:myapp"],
"app": {
"example.com/cap/myapp": [{"action": ["read"], "resources": ["items/*"]}]
}
}
]
}Enabling the plugin changes the behavior of the gen command of the goa tool.
The gen command output is modified as follows:
- Generates middleware that extracts the
Tailscale-App-Capabilitiesheader - Parses the JSON capabilities from the header
- Checks if the caller's grants satisfy the method's requirements
- Returns 401 if header is missing, 403 if permissions are insufficient
This plugin adds the following functions to the Goa DSL:
Requiredeclares that the method requires a Tailscale app capability with the specified action and resource.AllowAnonymousmarks the method as not requiring any capability check. Requests without the capabilities header will be allowed through.
The usage and effect of the DSL functions are described in the Godocs
Here is an example defining capability requirements at a method level.
var _ = Service("myservice", func() {
Method("list", func() {
// Requires the caller to have "read" action on "*" resource
tscap.Require("example.com/cap/myapp", "read", "*")
HTTP(func() { GET("/items") })
})
Method("create", func() {
// Requires the caller to have "write" action on "items/*" resource
tscap.Require("example.com/cap/myapp", "write", "items/*")
HTTP(func() { POST("/items") })
})
Method("health", func() {
// No capability check required
tscap.AllowAnonymous()
HTTP(func() { GET("/health") })
})
})Grants in Tailscale ACLs can use wildcards (*). The DSL specifies exact requirements:
| Grant Action | Required Action | Match? |
|---|---|---|
["*"] |
"read" |
Yes |
["read"] |
"read" |
Yes |
["write"] |
"read" |
No |
| Grant Resource | Required Resource | Match? |
|---|---|---|
["*"] |
"items/123" |
Yes |
["items/*"] |
"items/*" |
Yes (exact) |
["items/123"] |
"items/456" |
No |