Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

all: add support for custom caddy listener #1

Merged
merged 1 commit into from
Sep 19, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 2 additions & 29 deletions Caddyfile
Original file line number Diff line number Diff line change
Expand Up @@ -2,37 +2,10 @@
order tailscale_auth after basicauth
}

# The site on port 7000 demonstrates requiring Tailscale authentication, and
# using the placeholder values on the Caddy user object in a go template files
# (see index.html).
:7000 {
:80 {
bind tailscale/caddy
root .
file_server
templates
tailscale_auth
}

# The sites on port 7100 and 7101 demonstrate using Tailscale authentication
# with a proxied application. Port 7100 is the Caddy proxy which enforces
# authentication, uses the Caddy user object to set various X-Webauth headers,
# and then proxies the request to port 7101 which is the proxied application.
:7100 {
tailscale_auth
reverse_proxy http://localhost:7101 {
header_up X-Webauth-User {http.auth.user.tailscale_login}
header_up X-Webauth-Email {http.auth.user.tailscale_user}
header_up X-Webauth-Name {http.auth.user.tailscale_name}
header_up X-Webauth-Tailnet {http.auth.user.tailscale_tailnet}
header_up X-Webauth-Photo {http.auth.user.tailscale_profile_picture}
}
}

# The site on port 7101 is playing the role of a proxied application that
# doesn't know anything about Tailscale, but can be configured for external
# authentication in X-Webauth request headers. Real world applications that
# support this setup include Grafana and Gitea, among others.
:7101 {
root .
file_server
templates
}
186 changes: 129 additions & 57 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,82 +1,154 @@
# caddy tailscale auth
# Tailscale Caddy plugin

Using [nginx-auth][] together [with Caddy][] is pretty cool. But since Caddy
is written in Go anyway, what if we combined the two?
The Tailscale Caddy plugin brings Tailscale integration to the Caddy web server.
It's really multiple plugins in one, providing:

This repo provides a Caddy module that will talk with a local tailscaled in
exactly the same way that nginx-auth does, but without needing a separate
binary. Alternately, you can provide an auth_key and caddy will use tsnet to
join your tailnet directly and use that connection to authenticate users
(though tailscaled is still necessary for now to listen on the tailscale
interface).
- the ability for a Caddy server to directly join your Tailscale network
without needing a separate Tailscale client.
- a Caddy authentication provider, so that you can pass a user's Tailscale
identity to an applicatiton.
- a Caddy subcommand to quickly setup a reverse-proxy using either or both of
the network listener or authentication provider.

[nginx-auth]: https://github.com/tailscale/tailscale/tree/main/cmd/nginx-auth
[with Caddy]: https://caddyserver.com/docs/caddyfile/directives/forward_auth#tailscale

## demo
This plugin currently uses unreleased functionality in both Caddy and Tailscale
and is very experimental.

First, you'll need to install [xcaddy][] to build a caddy binary with custom modules.
## Installation

$ go install github.com/caddyserver/xcaddy/cmd/xcaddy@latest
Use [xcaddy](https://github.com/caddyserver/xcaddy) to build Caddy with the
Tailscale plugin included.

[xcaddy]: https://github.com/caddyserver/xcaddy
```
xcaddy build master --with github.com/tailscale/caddy-tailscale
```

Clone this repo and run `xcaddy run` to build and run a caddy server.
## Caddy network listener

**NOTE:** The first time you run it, you may be prompted for your password
because caddy uses your system certificate store to [install a certificate
authority] to sign development certificates.
Coming in Caddy 2.6, modules are able to provide custom network listeners. This
allows your Caddy server to directly join your Tailscale network without needing
a separate Tailcale client running on the machine exposing a network device.
Each site can be configured in Caddy to join your network as a separate node, or
you can have multiple sites listening on different ports of a single node.

[install a certificate authority]: https://caddyserver.com/docs/automatic-https
### Configuration

Once caddy starts up, open http://yourhost:7000/ in your browser. You actually
need to use your tailscale hostname or IP address; you can't use localhost.
Ideally, you should be greeted with your tailscale account info. Now visit
http://yourhost:7100/ and you should see the same information in a little
different format. This demonstrates two different ways that the module can be
used (see more details in [Caddyfile](./Caddyfile)).
Configure Caddy to listen on a special "tailscale" network address. If using a
Caddyfile, use the [bind directive](https://caddyserver.com/docs/caddyfile/directives/bind):

1. port 7000 - authenticate as normal and populate the standard caddy user object
2. port 7100 - do the same, but populate X-Webauth headers and proxy the
request to a separate application that knows nothing about Tailscale.
```
:80 {
bind tailscale/
}
```

Some other options you can try:
You can also specify a hostname to use for the Tailscale node:

Specify an auth key to use tsnet mode:
```
:80 {
bind tailscale/myhost
}
```

tailscale_auth {
auth_key ts-key-xxxxxxCNTRL-xxxxxx
If using the Caddy JSON configuration, specify a "tailscale/" network in your
listen address:

```json
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
"tailscale/myhost:80"
]
}
}
}
}
}
```

# or with an environment variable
tailscale_auth {
auth_key {env.TS_AUTH_KEY}
}
Caddy will join your Tailscale network and listen only on that network
interface. Multiple addresses can be specified if you want to listen on the
Tailscale address as well as a local address:

In order to use the environment variable form, you'll need to actually build a
caddy binary rather than just using `xcaddy run`. To do that:
```
:80 {
bind tailscale/myhost localhost
}
```

xcaddy build --with github.com/tailscale/caddy=./
TS_AUTH_KEY=ts-key-xxxxxxCNTRL-xxxxxx ./caddy run
Different sites can be configured to join the network as different nodes:

When using tsnet mode, you can also specify a custom hostname for your node as
well as verbose logging:
```
:80 {
bind tailscale/a
}

:80 {
bind tailscale/b
}
```
tailscale_auth {
auth_key ts-key-xxxxxxCNTRL-xxxxxx
hostname myhost
verbose
}

However, having a single Caddy site connect to separate Tailscale nodes doesn't
quite work correctly. If this is something you actually need, please open an
issue.

### Authenticating to the Tailcale network

New nodes can be added to your Tailscale network by providing an [Auth
key](https://tailscale.com/kb/1085/auth-keys/) or by following a special URL.
Auth keys are provided to Caddy via the `TS_AUTHKEY` or `TS_AUTHKEY_<host>`
environment variable. So if your network listener was `tailscale/myhost`, then
it would look for the `TS_AUTHKEY_MYHOST` environment variable, then
`TS_AUTHKEY`.

If no auth key is provided, then Tailscale will generate a URL that can be used
to add the new node and print it to the Caddy log. Tailscale logs can be
somewhat noisy so are turned off by default. Set `TS_DEBUG=1` to see the URL
logged. After the node had been added to your network, you can restart Caddy
without the debug flag.


## Caddy authentication provider

Setup the Tailscale authentication provider with `tailscale_auth` directive.
The provider will enforce that all requests are coming from a Tailscale user, as
well as set various fields on the Caddy user object that can be passed to
applications, similar to [nginx-auth][].

[nginx-auth]: https://github.com/tailscale/tailscale/tree/main/cmd/nginx-auth

The following fields are set on the Caddy user object:

- `user.id`: the Tailscale email-ish user ID
- `user.tailscale_login`: the username portion of the Tailscale user ID
- `user.tailscale_user`: same as `user.id`
- `user.tailscale_name`: the display name of the Tailscale user
- `user.tailscale_profile_picture`: the URL of the Tailscale user's profile picture
- `user.tailscale_tailnet`: the name of the Tailscale network the user is a member of

These can be mapped to HTTP headers passed to an application using something
like the following in your Caddyfile:

```
header_up X-Webauth-User {http.auth.user.tailscale_login}
header_up X-Webauth-Email {http.auth.user.tailscale_user}
header_up X-Webauth-Name {http.auth.user.tailscale_name}
```

tsnet mode still actually requires a local tailscaled running so that caddy can
listen on the tailscale network interface. We're looking into options to remove
the need for tailscaled at all.
When used with a Tailscale listener (described above), that Tailscale connection
is used to identify the remote user. Otherwise, the authentication provider
will attempt to connect to the Tailscale daemon running on the local machine.

# Related work
## tailscale-proxy subcommand

It looks like <https://github.com/astrophena/tsid> is very similar and did this
in mid 2021, but hooks into caddy as an http handler rather than an
authentication provider. It also doesn't support tsnet, which likely didn't
exist at the time.
The Tailscale Caddy plugin also includes a `tailscale-proxy` subcommand that
sets up a simple reverse proxy that can optionally join your Tailscale network,
and will enforce Tailscale authentication and map user values to HTTP headers.

For example:

```
xcaddy tailscale-proxy --from "tailscale/myhost:80" --to localhost:8000
```
2 changes: 1 addition & 1 deletion addresses.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.

package tsauth
package tscaddy

import (
"fmt"
Expand Down
17 changes: 15 additions & 2 deletions command.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.

package tsauth
package tscaddy

import (
"encoding/json"
Expand All @@ -30,6 +30,7 @@ import (
"github.com/caddyserver/caddy/v2/modules/caddyhttp/headers"
"github.com/caddyserver/caddy/v2/modules/caddyhttp/reverseproxy"
"github.com/caddyserver/caddy/v2/modules/caddytls"
"tailscale.com/util/strs"
)

func init() {
Expand Down Expand Up @@ -88,8 +89,20 @@ func cmdTailscaleProxy(fs caddycmd.Flags) (int, error) {
return caddy.ExitCodeFailedStartup, fmt.Errorf("--to is required")
}

// strip "tailscale/" prefix if present
from, tsBind := strs.CutPrefix(from, "tailscale/")

// set up the downstream address; assume missing information from given parts
fromAddr, err := httpcaddyfile.ParseAddress(from)

var listen string
if tsBind {
listen = "tailscale/" + fromAddr.Host + ":" + fromAddr.Port
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wonder if this will work OK with IPv6

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like it? I just tested with caddy tailscale-proxy --from "tailscale/caddy:2000" --to :8000 and was able to access via tailscale ipv6 address.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, sorry, I meant caddy tailscale-proxy --from <ip6_addr> e.g. --from 6240:3c33:...

I suspect the IPv6 address would have to be surrounded in [ ], which isn't bad (and is probably more correct) but users should just be aware if they do something like that.

Glad it's working over IPv6 though!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nope... fails to parse if ipv6 address is specified. I'll have to look into that

fromAddr.Host = ""
} else {
listen = ":" + fromAddr.Port
}

if err != nil {
return caddy.ExitCodeFailedStartup, fmt.Errorf("invalid downstream address %s: %v", from, err)
}
Expand Down Expand Up @@ -172,7 +185,7 @@ func cmdTailscaleProxy(fs caddycmd.Flags) (int, error) {

server := &caddyhttp.Server{
Routes: caddyhttp.RouteList{authRoute, route},
Listen: []string{":" + fromAddr.Port},
Listen: []string{listen},
}

httpApp := caddyhttp.App{
Expand Down
Loading