To help you authenticate traffic between Fly Apps, we’ve added a new Fly-Src
headers to Flycast requests (i.e. requests between Fly Apps that go through our Proxy).
Flycast is one of my favorite Fly.io features. It allows you to send requests between Fly Apps via Fly Proxy, letting you to take advantage of features like geographically aware load balancing and autostart/autostop based on backend traffic. The best part: you can even use it for traffic between organizations. This allows for powerful design patterns where apps running especially sensitive or especially dangerous workloads can be isolated in their own organization while allowing narrowly scoped access from the rest of your apps.
In such a design, you still might need to implement authentication for requests between apps, so the server can know who it’s talking to. To make this easier, we’re now injecting a header on Flycast HTTP requests that includes information about the client that can be used to make authorization decisions.
Fly-Src: instance=<machine-id>;app=<app-name>;org=<org-slug>;ts=<timestamp>
Other apps in the same organization can still send traffic directly to your service, bypassing Fly Proxy and setting whatever Fly-Src
they want. To give assurance that the Fly-Src
header is actually coming from Fly Proxy, we also set a Fly-Src-Signature
header, which contains a base64 encoded ed25519 signature of the Fly-Src
header. You can find the hex encoded public key for verifying this signature in your machines at /.fly/fly-src.pub
.
Fly-Src
tells you with confidence which machine/app/organization sent a request, but that might not be sufficient for your purposes. A Server-Side Request Forgery vulnerability in a trusted client application could allow attackers to send requests to your server and they would receive a valid Fly-Src
too. As with any security tool, understand its limitations as well as your needs before employing it.
Example
Here’s a simple Ruby webapp that verifies and parses the Fly-Src
/Fly-Src-Signature
to learn which app made a request:
require "sinatra"
require "ed25519"
require "base64"
get "/" do
client_app = fly_src["app"]
"Hello, #{client_app}!"
end
# The hex encoded ed25519 public key is provided at /.fly/fly-src.pub
PUBLIC_KEY = Ed25519::VerifyKey.new([File.read('/.fly/fly-src.pub')].pack('H*'))
# Returns a verified Hash of information about the calling application.
def fly_src
# Get Fly-Src and Fly-Src-Signature headers from request
src = request.env["HTTP_FLY_SRC"]
sig = request.env["HTTP_FLY_SRC_SIGNATURE"]
halt(401, "not flycast") if src.nil? || src.empty? || sig.nil? || sig.empty?
# Decode and verify the signature
begin
PUBLIC_KEY.verify(Base64.strict_decode64(sig), src)
rescue ArgumentError, Ed25519::VerifyError
halt(401, "bad signature")
end
# Parse the Fly-Src header (`instance=<machine-id>;app=<app-name>;org=<org-slug>;ts=<timestamp>`)
parsed = src.split(";").map { |kv| kv.split("=") }.to_h
parsed["ts"] = Time.at(parsed["ts"].to_i)
# Check that the timestamp is recent to prevent replay attacks
halt(401, "expired") if Time.now - parsed["ts"] > 10
parsed
end