Fly-Src: Authenticating HTTP requests between Fly Apps

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
9 Likes