Cloudflare - SSH to Origin IP

tl;dr

A serverfault.com post outlined how you can use Cloudflare's API to query for your Origin IP and use that for SSH. However, they didn't account for multiple Zones, and they didn't account for SSH'ing to subdomains like foo.example.com (though nor do I for now).


The details

Cloudflare is both a DNS service and HTTPS reverse proxy. You can "gray-cloud" your DNS entries such that querying them will return your "real" (typically called Origin IP). But Cloudflare is at its best when you "orange-cloud" your DNS entries, which actually returns IP addresses owned by Cloudflare that then perform a reverse proxy to your Origin.

The protection here is at least two-fold:

  1. Your Origin IP is not revealed
  2. Cloudflare absorbs malicious traffic inbound for your service

But orange-clouds have their own set of problems. Namely, a limited set of ports are actually proxied to your Origin, and even then only for HTTPs.

SSH'ing to your domain names won't work

Imagine you own example.com at IP 1.2.3.4 and expose both HTTP/S (TCP 80 and 443) and SSH (TCP 22) services. You'd probably access HTTPS and SSH via:

# Works, as example.com resolves to your Origin IP (1.2.3.4), and
# you expose TCP 80 & 443.
$ curl -v example.com
* Connected to example.com (1.2.3.4) port 80 (#0)


# Works, as example.com resolves to your Origin IP (1.2.3.4), and
# you expose TCP 22.
$ ssh -v example.com
debug1: Connecting to example.com [1.2.3.4] port 22.  
debug1: Connection established.  

But when you move to Cloudflare with an orange-cloud, you'll notice that non-HTTPS and non-proxied ports traffic no longer works:

# Works, as example.com resolves to Cloudflare IPs (2.3.4.5), and
# they expose TCP 80 and 443
$ curl -v example.com
* Connected to example.com (2.3.4.5) port 80 (#0)


# Busted, as example.com resolves to Cloudflare IPs (2.3.4.5), and
# they don't proxy TCP 22
$ ssh -v example.com
debug1: Connecting to example.com [2.3.4.5] port 22.  
debug1: connect to address 2.3.4.5 port 22: Connection timed out  
ssh: connect to host example.com port 22: Network is unreachable  

Solution options

  1. Hardcode your Origin IP: ssh 1.2.3.4.
  2. Add gray-clouded subdomain: ssh noproxy.example.com.
  3. Use Cloudflare API to resolve your Origin IP.

Hardcode your Origin IP

You can still SSH to your Origin by using its IP address:

# Works because this is direct access, bypassing Cloudflare
$ ssh -v 1.2.3.4
debug1: Connecting to 1.2.3.4 [1.2.3.4] port 22.  
debug1: Connection established.  

But the downsides are:

  1. You must remember or write down your Origin IP
  2. If your Origin IP changes, you must manually update it

Gray-clouded subdomain

You could use a subdomain whose sole purpose is to provide "traditional" DNS resolution by using a gray-cloud and A'ing or CNAME'ing it to your Origin IP.

This means example.com will return Cloudflare's IPs, and connections will still be proxied. But noproxy.example.com will return your Origin's IP, and connections will not be proxied:

# Works, proxied through Cloudflare
$ curl -v example.com
* Connected to example.com (2.3.4.5) port 80 (#0)


# Works, no proxy through Cloudflare, connects directly to your Origin
$ curl -v noproxy.example.com
* Connected to example.com (1.2.3.4) port 80 (#0)


# Works, no proxy through Cloudflare, connects directly to your Origin
$ ssh -v noproxy.example.com
debug1: Connecting to noproxy.example.com [1.2.3.4] port 22.  
debug1: Connection established.  

The downsides are:

  1. Cloudflare is providing no security
  2. You reveal your Origin IP to anyone who queries for noproxy.example.com

Cloudflare API

Since Cloudflare has an API, and it knows your Origin IP in order to reverse proxy incoming connections, you can ask Cloudflare for your Origin IP before setting up an SSH connection.

This means you can retain your orange-cloud status, along with hiding your Origin IP to the public and leverage Cloudflare's security!

With that in mind, there are ~2 API calls to make in order to get the necessary information, both of which require API Permissions:

  1. Find example.com's Zone ID (Zone.Zone.Read)
  2. Use example.com's Zone ID to lookup the Origin IP address for the root domain (Zone.DNS.Read)

I decided to allow this API Token to work for all my Zones, but you can restrict its scope to all Zones belonging to a specific account, or even a list of single Zones.

Note: You could skip creating this API Token and use your Global API Key instead, but I would advise against it. Your Global API Key is akin to your account's password, and using this scoped API Token is following the principle of least privilege.

Using the Github Gist from above, and the cforigin function:

# Get Origin IP
$ IP=$(cforigin example.com) && echo $IP
1.2.3.4


# SSH to Origin
$ ssh -v $IP
debug1: Connecting to 1.2.3.4 [1.2.3.4] port 22.  
debug1: Connection established.  

Gotchas

  1. There can be more than one IP address that represents "Origin", as exampled in the Github Gist. It is up to the user to select the "correct" one to use.
  2. cforigin currently doesn't support CNAME'd subdomains. If you wanted to connect to foo.example.com, the API calls currently do not resolve this.
  3. cforigin currently doesn't support more than 2 top-level domains (TLDs). For instance, example.com.uk will incorrectly attempt to resolve com.uk.