There are a number of ways to share private crates between Rust projects. The easiest way is to use Git-based dependencies, but granting GitHub Actions access to private Git repositories is surprisingly hard. What you really want is your own private Cargo repository.
Amos Wenger (aka fasterthanlime) has a wonderful article that covers all sorts of Rust productivity tips, including creating your own private crate registry. Reading that article is well worth your time, especially since the primary job of this article is to expand on the solution in that article with a slightly more turnkey option.
Amos’s approach works great, but there were a couple of things I wanted to change:
- Colocate the cargo registry and the Git index. The approach detailed in the aforementioned article has you store the Cargo Git index on GitHub, which means that GitHub Actions needs access to private repos (one of the problems that sent us down this path to begin with).
- Eliminate shared secrets. The private registry needs to be protected, otherwise you are exposing your source code to the Internet. Amos uses HTTP basic auth and IP allowlists to protect his registry, but I wanted to see if I could come up with something more secure that did not involve sharing a secret (even inside of a private repo).
Both of those changes have one thing in common: they involve securing
and authenticating the network connection between
cargo and the
Making auth someone else’s problem
Tailscale lets you, in their words, “easily manage access to private resources.” That sounds exactly like what we are trying to do. I have been using Tailscale for years, and it does exactly what it says: makes it easy to securely connect devices, regardless of their physical location.
Tailscale offers an Access Control List (ACL) feature that, as an individual managing their own devices, I had never needed to use. I also wasn’t sure that this functionality was at the right layer in the stack; wasn’t it safer to manage ACLs on each device?
Deploying a private Cargo registry opened up a whole new set of challenges that changed my perspective on this feature. Specifically, Tailscale ACLs give me a way to allow semi-untrusted devices into the private network. This meant that I could put the GitHub Actions runners on the same virtual network as my internal development machines, but limit their access solely to the private Cargo registry.
This also solved the larger authorization problem with the crate registry, in that I could simply let it run un-authenticated. Tailscale would ensure that only authorized users (and GitHub) could get at the crate registry.
The one wrinkle in all of this
We need to run a bunch of processes on the same machine in order to pull this off:
- Ktra for the private crate registry.
- git-http-backend (a CGI) for exposing the cargo registry.
- NGINX for running the CGI and proxying requests to Ktra.
tailscaledfor making all of this accessible on our Tailnet.
- And to really do this right we should include a script that properly initializes a blank Fly.io volume with the Ktra repository.
This is the point where you think, “That sounds like a lot of accidental complexity, you should leave this alone and get back to building your product.” But I sort of squinted and decided that our product also had the same problem of needing to run multiple processes on Fly.io, and so I had all the justification I neeeded to create Ground Control.
Ground Control has its own post, but in a nutshell, Ground Control makes it easy to run multiple processes inside of a single Docker container (or Fly.io micro-VM).
Super Guppy: Your Cargo transport in the clouds
Super Guppy is the result of this effort: a Docker image that makes it easy to run your own private Cargo registry. You do need Tailscale, and a place to host the registry (such as Fly.io), but those are both free to try, and are very affordable for small teams.
aka “Let’s really make auth someone else’s problem!”
You still have to create accounts in Ktra to use Super Guppy. That’s fine, but it turns out that Tailscale happens to know which user is associated with each active connection. This means you can do things like seamlessly authenticate to Grafana using Tailscale.
I am certain that Ktra could be modified to look for Tailscale authorization (perhaps in a generic auth header?), similar to the OpenID auth feature that was recently added to Ktra. This would eliminate the need to provision or manage user accounts in Ktra; everything would come from Tailscale.
A big shout-out to Amos, who deserves all of the credit for explaining Ktra in such a clear manner. Reading his article, and working through his examples, was what made it possible for me to figure all of this out and create Super Guppy.