Page MenuHomePhabricator

CloudFront configuration is more involved than the documentation implies
Open, Needs TriagePublic

Description

I am trying to configure the alternate file domain for my local Phabricator install. I have encountered a problem. My Phabricator, https://code.simplyinsured.com/, is configured with security.alternate-file-domain of https://d3vd7xwqgmo7c1.cloudfront.net/. I've configured that Cloudfront domain to forward to code.simplyinsured.com.

This should be a link to a picture of a dancing bear, however it is actually a link to a rendered 404 page: https://d3vd7xwqgmo7c1.cloudfront.net/file/data/i3glh7w6o3gu6jgyxi7d/PHID-FILE-mb3mj26stq5bvkslvazy/breakdancingbear

Why is this happening? It seems to be related to authentication, but why is Phabricator requiring authentication for these?

https://d3vd7xwqgmo7c1.cloudfront.net/res/* seems to work correctly, e.g. core.pkg.js.
https://d3vd7xwqgmo7c1.cloudfront.net/file/* seems to always return a 404.

See P1841.

Event Timeline

ry raised the priority of this task from to Needs Triage.
ry updated the task description. (Show Details)
ry added a subscriber: ry.

Is BehaviorsEdit BehaviorsForward Headers set to "None (Improves Caching)"?

Particularly, note:

Once configured, accessing the distribution's domain name should return a Phabricator error page indicating that Phabricator does not recognize the domain. If you see this page, it means you've configured things correctly.

However, I don't see this page when accessing your distribution:

https://d3vd7xwqgmo7c1.cloudfront.net/

Screen Shot 2015-08-15 at 8.09.53 AM.png (1×1 px, 125 KB)

Here's the error page you should see:

https://d27urz3c38hyx4.cloudfront.net/

Screen Shot 2015-08-15 at 8.10.19 AM.png (1×2 px, 235 KB)

The fact that Cloudfront serves a login page instead of an error page suggests that:

  • The "Host" header is being incorrectly forwarded; or
  • you've configured Cloudfront as the phabricator.base-uri (this is undesirable and won't work); or
  • you've added Cloudfront to phabricator.allowed-uris (this is also undesirable and probably won't work, although I'm not sure which behavior wins offhand).

Actually, I think the setting I pointed out is unlikely to affect behavior here. I'm not sure how the request is reaching your server with a "Host" header which allows Phabricator to serve the request. Are you forcing this on the server (e.g., overwriting the inbound Host header with the "correct" header)?

Generally, the expectation is:

  • File requests should reach the server with the CloudFront "Host: blah.cloudfront.net" header intact. This should be the default behavior of CloudFront. If this isn't happening, Phabricator won't know that it needs to serve CDN requests. It uses the Host header to distinguish between CDN and non-CDN requests, and can not use any other mechanism safely (it is unsafe to serve CDN requests from some "special" URI on the main domain).
  • These requests should produce an error when made to the root path, as above.
  • The security.alternate-file-domain should not be listed in phabricator.allowed-uris.

Forward Headers was set to "None (Improves Caching)". I changed it to "Whitelist" and allowed the "Host" header. Now when I connect to https://code.simplyinsured.com/ and send a Host of "d3vd7xwqgmo7c1.cloudfront.net", I get the expected error page you described, and I'm able to download my dancing bear picture.

However, Cloudfront no longer serves files on my domain, at all. I simply get a "CloudFront wasn't able to connect to the origin." error message. See P1843.

There's one detail I haven't mentioned yet: nginx is listening on the IP and is configured to reverse proxy (passing along all original headers) both the code.simplyinsured.com and d3vd7xwqgmo7c1.cloudfront.net domains to Phabricator. It seems unlikely to me that this is the source of the problem since in the above test, connecting to the IP with the correct Host header results in the correct expected behavior.

Since this is a connect error and only happens now that I'm passing along the Host header, it seems likely to me that there's some SSL configuration problem. I'm going to continue investigating since it seems like Phabricator is now working as expected, but would be happy to hear any advice on the Cloudfront configuration problem.

After doing some packet capture on my server to see exactly what's happening, I've found that when I configure CloudFront to forward the Host header (or All Headers), it will connect to my origin (code.simplyinsured.com), but the SSL handshake server name field will be set to d3vd7xwqgmo7c1.cloudfront.net. Since my server doesn't have a certificate for *.cloudfront.net, there's no way that's going to result in a valid SSL handshake.

Did something change on CloudFront's end? I don't understand how the documentation could ever work over HTTPS given this behavior.

Can you replicate issues outside of a reverse proxy scenario?

Ah, sorry, I was mistaken. Our CloudFront setup is actually more involved than the documentation implies. This host has a simpler setup but uses CloudFlare, not CloudFront; CloudFlare applies weaker SSL certificate validation rules. Here's how we fix the headers before the request runs:

$cloudfront_domains = array(
  'admin.phacility.com' => 'd27urz3c38hyx4.cloudfront.net',
  'content.phacility.com' => 'd3cn662t4iw3q4.cloudfront.net',
);
$cloudfront_default = $cloudfront_domains['content.phacility.com'];

// If this request is coming from within the cluster, we'll trust HTTP
// headers added by the load balancer. We do not trust these headers for
// requests which do not originate from within the cluster, because they
// are client-controlled if the request is being received directly.
if (PhacilityServices::isClusterRequest()) {
  // Since we terminate SSL at the ELB, requests are normally HTTP by the
  // time they reach the server, even if they were originally HTTPS. The
  // ELB sets this header to indicate that the original request was HTTPS.
  if (idx($_SERVER, 'HTTP_X_FORWARDED_PROTO') == 'https') {
    $_SERVER['HTTPS'] = true;
  }

  $forwarded_for = idx($_SERVER, 'HTTP_X_FORWARDED_FOR');
  if ($forwarded_for) {
    // This may be a list of IPs, like "1.2.3.4, 4.5.6.7", if the
    // request the load balancer received also had this header. In
    // particular, this happens routinely with requests received through
    // the CloudFront CDN.

    // We only care about or trust the last IP in the list: the others are
    // controlled by the client.
    $forwarded_for = explode(',', $forwarded_for);
    $forwarded_for = last($forwarded_for);
    $forwarded_for = trim($forwarded_for);
    $_SERVER['REMOTE_ADDR'] = $forwarded_for;
  }

  // If this is a CloudFront request, treat the host as the CloudFront
  // domain. This makes sure that we only serve things which we would serve
  // to the file domain and don't cache anything unexpected in the CDN.
  // (It's not a big deal if we do, since CloudFront won't forward or cache
  // cookies, but it's cleaner to make sure this request is executing in
  // an accurate context.)
  $user_agent = idx($_SERVER, 'HTTP_USER_AGENT');
  if ($user_agent == 'Amazon CloudFront') {
    $host = idx($_SERVER, 'HTTP_HOST');
    if (isset($cloudfront_domains[$host])) {
      $_SERVER['HTTP_HOST'] = $cloudfront_domains[$host];
    } else {
      $_SERVER['HTTP_HOST'] = $cloudfront_default;
    }
  }
}

Roughly:

  • Forward no headers.
  • Use trusted source IP + user-agent to correct the Host header (and, in the code above, the remote address, for completeness) before the request runs.

You can probably put something like this in support/preamble.php. See:

https://secure.phabricator.com/book/phabricator/article/configuring_preamble/

I'll fix the documentation.

epriestley renamed this task from Alternate file domain isn't allowed to serve files to CloudFront configuration is more involved than the documentation implies.Aug 17 2015, 1:07 AM

also, I would like to see the breakdancingbear

Great! I'm not sure how to use a trusted source IP, as CloudFront doesn't seem to provide any configuration in that regard, but just using the user-agent should not result in any reduced security, since no authenticated actions can be performed when this Host is set.

I ended up configuring this in my reverse proxy because it was the easiest way I knew how to. The configuration looks like so:

# We instruct Phabricator that it should serve static assets and uploads
# without authentication by reaching it with our CloudFront host header. When
# accessed from this domain, Phabricator will refuse to set cookies and so
# nothing authenticated could possibly happen.
map $http_user_agent $rewritten_host {
	default               $host;
	"Amazon CloudFront"   d3vd7xwqgmo7c1.cloudfront.net;
}

server {
	# ...
	proxy_set_header Host $rewritten_host;
	# ...
}

The dancing bear lives: https://d3vd7xwqgmo7c1.cloudfront.net/file/data/i3glh7w6o3gu6jgyxi7d/PHID-FILE-mb3mj26stq5bvkslvazy/breakdancingbear

FWIW this affected us as well, we switched from cloudflare back to cloudfront and hit this snag. The default settings do not forward Host header, we had to add that ourselves. Didn't need to change anything else, though - it seems that cloudfront performs loose SSL validation and is OK with the certificate/host mismatch. (Maybe it at least requires the host to match the origin domain - not sure.)