tclip: A pastebin for your tailnet

Photo of Xe Iaso

As a developer, there are many times when you need to share little snippets of text with your friends or team. The tool I used the most for this is GitHub gists. This lets you upload little snippets of text, and get a publicly shareable link to it.

While I was playing around with tsnet, I got the idea to make a little pastebin clone using it. And then Funnel was announced. The hacker gear started turning, and I did a little exploration of the problem space and came up with tclip. I wanted to make a tool that I would feel comfortable with using every day, if I got to bring in a bunch of other cool technology into it as well, that would be even more cool.

So I ended up with tclip.

The homescreen of tclip, showing a textbox labeled 'create new paste'

The core of the entire program can be summarized in this entire image. It is a box that you can type text in, and submit it to the server. Then you get a link that you can use to share with your friends and coworkers. That's it. That's the entire program.

If you put code in the box and then name the file appropriately, your file will be syntax highlighted. There's a command line tool that you can use to pipe the output of commands or upload files directly to your tclip server so you can share them with your team and friends.

If you click on the List link in the top, you can see a list of all the pastes that have been created recently, but the main screen will show the last 5 pastes just so you can get to the one you want to see a little bit quicker. If you put markdown in the box and end your filename in .md, it'll be rendered as HTML for you. We've hidden a few other things to spark joy like this, like an Emacs package and the ability to run as a systemd portable service.

An albino pidgeon behind a normal one as found on Oberbaumbrücke in Berlin. Photo by Xe Iaso, 2023.
An albino pidgeon behind a normal one as found on Oberbaumbrücke in Berlin. Photo by Xe Iaso, 2023.


The easiest way to use tclip is to install it on

First, get an authkey from the Keys page of the admin console. It is a good idea to associate this with the tag tag:service or its own tag:tclip. Do not set the ephemeral flag as that will destroy the node when the service shuts down. This almost certaintly is not what you want.

Make sure you have MagicDNS enabled from the DNS page of the admin console, see the MagicDNS KB article for more information about how MagicDNS works.

Enabling MagicDNS is required for most of the defaults for the tclip command and the Emacs package to work.

You will need to have a account (this application is designed to fit within their free tier).

In one of your infrastructure management GitHub repositories, create a folder for tclip and then copy the following fly.toml template into that folder.

app = "FLY_APP_NAME"
image = ""
strategy = "immediate"
DATA_DIR = "/data"
source = "tclip_data"
destination = "/data"

Replace FLY_APP_NAME with a name such as yourorg-tclip and then run these commands with the flyctl command:

$ flyctl apps create FLY_APP_NAME
$ flyctl volumes create tclip_data
$ flyctl secrets set TS_AUTHKEY=<key>
$ flyctl deploy

You should be able to open the app at http://paste and paste to your heart's content.

There are more detailed instructions on the tclip GitHub repo if you want to deploy to your own host running Docker or a Linux machine running a modern version of systemd.


If you know how to use a web form, you know how to use tclip. Paste your text into the box. Give it a filename. Hit submit. Share the link. There is no next step. It will be available on your tailnet for whoever wants to access it.

If you want to limit access to your tclip server, you can do so with Tailscale ACL rules.

All pastes are automatically attributed to the person that posted them, thanks to Tailscale already knowing who you are.

The Tailscale magic: Funnel integration

However, it doesn't stop there. The real magic comes when we integrate this with Funnel so that you can share your pastes publicly. If you enable Funnel egress with the USE_FUNNEL=true environment variable (or the --use-funnel flag), tclip will register itself publicly with funnel to allow people to view only the following:

  • An index page that explains what the tclip server is and what it is used for
  • Individual paste pages without a list of all of them
  • Static CSS and JavaScript required for the site to function

If you enable this feature, people outside your organization will be able to read individual pastes. Depending on the facts and circumstances of how people in your organization use tclip, this could create a data exfiltration risk. Each tclip paste has a unique UUIDv4 assigned to it upon creation, so the likelihood of an attacker guessing a paste ID is very low (1 in a 2**112 chance, or about the likelihood of proving that a single arbitrarily chosen grain of sand is the same grain of sand across an entire galaxy).

This feature was mainly intended for personal use (such as hosting it on your private tailnet to replace your use of GitHub gists), but some organizations may find it useful for standing up quick HTML pages with Markdown.

the avatar for Xe Iaso

I feel that this is the huge value proposition of using tsnet and funnel together like this. Create a service for your team and then expose part of it to the world at large. I don't know of anything else that lets you establish hard security boundaries like this and I'm excited to see what could happen if this was the norm, not the exception.

This separation is enabled at this code level with how the tailnet routes and the public-facing routes are split out:

tailnetMux := http.NewServeMux()
tailnetMux.Handle("/static/", http.FileServer(http.FS(staticFiles)))
tailnetMux.HandleFunc("/paste/", srv.ShowPost)
tailnetMux.HandleFunc("/paste/list", srv.TailnetPasteIndex)
tailnetMux.HandleFunc("/api/post", srv.TailnetSubmitPaste)
tailnetMux.HandleFunc("/api/delete/", srv.TailnetDeletePost)
tailnetMux.HandleFunc("/", srv.TailnetIndex)
tailnetMux.HandleFunc("/help", srv.TailnetHelp)
funnelMux := http.NewServeMux()
funnelMux.Handle("/static/", http.FileServer(http.FS(staticFiles)))
funnelMux.HandleFunc("/", srv.PublicIndex)
funnelMux.HandleFunc("/paste/", srv.ShowPost)

The tailnetMux contains routes that are exposed to your tailnet and the funnelMux contains routes that are exposed to the public internet. This lack of cross-pollenation means that there is a hard security barrier between internal only routes (like creating new pastes) and external routes (showing the contents of a single paste).

We hope you enjoy using tclip! We've been working hard on it to make it a tool that you can rely on, as well as a way to explain advanced ways of using Tailscale.

If you run into any issues, open an issue on our GitHub page tailscale-dev/tclip. We're more than happy to help you.