Whenever we come across implementing file uploading functionality in Rails, we either use the CarrierWave or the PaperClip gem. Fog is the choice for Amazon S3. This setup works fairly well, easy to setup and runs successfully on production in a day.

Background

For security reasons, Amazon S3 configuration for CarrierWave has a configuration option ‘fog_authenticated_url_expiration‘ which defines the seconds to live for the URL, when used with ‘fog_public = true‘. This enables CarrierWave to generate a signed URL which can be accessed for the defined seconds only, after which the S3 document URL expires. This is provided for security reasons. When we want to upload private data on the S3 cloud buckets, such an arrangement keeps the data secure from guest users.

What happens when the URL expires?

In real time, when user has opened up a page which has private S3 download URLs and stayed inactive for sometime, the link timeout will expire it. Now, when user clicks on the link, it tries to open the document but S3 would throw ‘AccessDenied‘ error and show an error page with an XML content.

<Error>
  <Code>AccessDenied</Code>
  <Message>Request has expired</Message>
  <X-Amz-Expires>300</X-Amz-Expires>
  <Expires>2016-08-05T13:05:37Z</Expires>
  <ServerTime>2016-08-17T14:01:50Z</ServerTime>
  <RequestId>5e674uth3q4</RequestId>
  <HostId>
    XERYHFXDZHBDFHt7e45ysetrzhdfy
  </HostId>
</Error>

This appears annoying and the user experience deteriorates. To handle such a case gracefully, one of the following needs to be done.

  1. Make the download URLs public – This is not feasible most of the times.
  2. Increase the time to expire, which just decreases the probability of this error, but does not really fix it, its just a work around.
  3. Implement a page instead of this XML response. – Bang!

Solution

For point 3 defined above, Amazon has another service called CloudFrontwhich allows us to define custom error page and rules to redirect to it on specific HTTP errors. But as usual, Amazon has “amazing” documentation to for help!

Never-mind, There are some really nice articles which explain the whole process of configuring CloudFront.

Custom Error Pages and Responses for Amazon CloudFront | AWS Blog
You can create a separate custom error response for each of the ten HTTP status codes listed in the menu. The Response… aws.amazon.com

But, What next?

Configuring CloudFront just does not plug it with the S3 configuration for CarrierWave. First thing is, CloudFront has created a CDN URL for us, but to use that URL, we need to replace the domain name in S3 URL.

So, “https://s3.amazonaws.com/exampleBucket/26b8dbda-32a8-4e54-99d8.txt?X-Amz-Date=20160805T130037Z&X-Amz-Expires=300…” should be changed as “https://my-new-id.cloudfront.net/exampleBucket/26b8dbda-32a8-4e54-99d8.txt?X-Amz-Date=20160805T130037Z&X-Amz-Expires=300…“.

This URL would be accessible, and to change it at where its generated we have to use the following property in ‘carrier_wave.rb’ initializer.

config.asset_host = "https://my-new-id.cloudfront.net"

But this does not work with ‘config.fog_public = true‘ option, and we don’t want to make it public! Its a bottleneck!

How to generate CloudFront signed URL with CarrierWave?

cloudfront-signer gem to the rescue! Yes, there is another gem which provides the interface to create signed URL (similar to S3 URLs) with CarrierWave. We need to follow the steps below.

  • Create CloudFront keys – From amazon console, click on “<your name>” dropdown -> “security credentials” -> “CloudFront key pairs” -> “create new key pairs”
  • Download the key pairs and store it at a secure place.
  • Install cloudfront-signer gem, and instead of fog configuration add new cofnguration for AWS as following,
CarrierWave.configure do |config|
  
  config.storage  = :aws
  config.aws_bucket = Rails.env.development? ? 'dev0bucket' : 'prod-bucket'
  config.aws_acl  = 'public-read'
  
  config.aws_attributes = {
   expires: 10.minutes.from_now.httpdate,
   cache_control: 'max-age=604800'
  }
  
  config.aws_credentials = {
   access_key_id:   Settings.carrier_wave.amazon_s3.access_key,
   secret_access_key: Settings.carrier_wave.amazon_s3.secret_key,
   region:      'us-standard' # Required
  }
  
  config.asset_host = "https://my-new-id.cloudfront.net"
  
  config.aws_signer = -> (unsigned_url, options) {       
    Aws::CF::Signer.sign_url(unsigned_url, options) 
  }
end
  • Reboot the rails application, and it should generate appropriate CloudFront CDN based URLs.
  • Open a document in a new tab, change the URL or wait for 10 minutes till it expires. Then try again accessing it. It should redirect and show the custom error page as you have defined.

This way, we can show a custom error or notification pages for diff. HTTP errors while accessing Amazon S3. CloudFront provides a better interface instead of just showing the XML error, this could be helpful to non-techie users.

References

Srool The Knife: Using Signed URLs with CloudFront, CarrierWave and Rails
Edit description www.srooltheknife.com

Use CDN with carrierwave + fog in s3 + cloudfront with rails 3.1
I’m using fog with carrierwave in my website. But the images load very very slowly. Then I want to speed up loading of… stackoverflow.com

leonelgalan/cloudfront-signer
cloudfront-signer – Ruby gem for signing AWS CloudFront private content URLs and streaming paths. github.com

Click here for more details…

At BoTree Technologies, we build enterprise applications with our RoR team of 25+ engineers.

We also specialize in Python, RPA, AI, Django, JavaScript and ReactJS.

Consulting is free – let us help you grow!