SuperGeekery: A blog probably of interest only to nerds by John F Morton.

A blog prob­a­bly of inter­est only to nerds by John Morton.

20Jul2021

Tweak­ing site per­for­mance and con­fig­ur­ing AWS

I’ve recent­ly updat­ed jmx2​.com. The site worked as it was and I did­n’t real­ly have the spare time to get to the ever-grow­ing list of updates I’d accu­mu­lat­ed. I had a week of rel­a­tive qui­et appear and I’ve final­ly worked on the site again. Here’s what I’ve been doing.

  • Gen­er­al clean up of the code in the Craft Twig templates.
  • Built a new serv­er with PHP 8 and moved the site.
  • I had already been using a picture ele­ment to serve dif­fer­ent sized images, but I’ve final­ly added webp images to those tags. (No avif sup­port yet though.)
  • Cor­rect­ed an error in my sizes attribute that was serv­ing over­sized images to some mobile devices. 
  • Updat­ed the build sys­tem from Lar­avel Mix to Vite.
  • Updat­ing my local devel­op­ment work­flow allowed me to serve mod­ern Javascript to mod­ern browsers and serve Babel-con­vert­ed JS to old browsers.
  • The new work­flow also pro­vides dynam­i­cal­ly cre­at­ed Crit­i­cal CSS for the pages on the site.

These updates brought the Page­Speed score for jmx2​.com from the mid-80s to the high 90’s.

Most of the updates hap­pened in Craft and my Twig tem­plates. There were a few AWS-spe­cif­ic things that I need­ed to solve in this process and that’s the pur­pose of this post.

A CDN for images

I moved all of the images for the site to a CDN. In Craft, I set up an Ama­zon S3 buck­et which then gets dis­trib­uted to a Cloud­Front URL that has a domain of cloud.jmx2.com asso­ci­at­ed with it.

Set­ting caching time for pre-exist­ing and new­ly-uploaded assets

Page­Speed Insights revealed I had not set a max-age for the cache for the images being served from CloudFront.

I store my site assets in a direc­to­ry called site-assets so I added a Cache-Con­trol” head­er with the val­ue of max-age=31536000 to all the files in that direc­to­ry. In the list of objects, select the direc­to­ry or spe­cif­ic files you want to add the meta­da­ta on then select the Actions drop­down and then Edit metadata”.

Why 3153600? The max-age is an inte­ger of sec­onds; 365 days is made up of 3153600 sec­onds. So, I’m caching images for a full year.

Cache control max age

Setting the max-age of assets served from Amazon S3

The set­ting above set the head­er on all of the pre-exist­ing assets that I man­u­al­ly copied over. 

New assets uploaded direct­ly from Craft CMS need to have their cache dura­tion set as well. The Ama­zon S3 plu­g­in has a set­ting for this when you cre­ate a vol­ume. I’ve set my cache dura­tion for 1 year here as well.

2021 07 24 10 01 15

Cache Duration for setting up the volume in Craft CMS

With that fix in place, my Page­Speed score was improved as you can see in the next screen­shot, but keep read­ing where I fix a poten­tial prob­lem I have created.

Jmx2 com pagespeed 98 13 JUL2021

Final PageSpeed Insight score of 98

Allow­ing image embeds while pro­tect­ing fonts

I want my images to be able to be served to any site so that media embeds work. For exam­ple, if some­one shared one of our projects on Face­book, I want the social media image to appear.

facebook-debugger-google-post-20JUL2021

The Facebook Sharing Debugger

I also host my fonts on Cloud­Front. I want to restrict how those files are used. I’d like to have them served only to jmx2​.com.

My ini­tial thought was a restric­tive ref­er­er head­er. But, if I only allowed jmx2.com to be a valid ref­er­er head­er, that would pre­vent social embeds from show­ing images for the posts. (Triv­ia: Ref­er­er’ is mis­spelled in the HTML spec, so we must use the mis­spelled ver­sion of the word now! 🤷)

There are also issues with adding a ref­er­er head­er to Cloud­Front assets. For full details see this 2016 post in the AWS Secu­ri­ty Blog: How to Pre­vent Hotlink­ing by Using AWS WAF, Ama­zon Cloud­Front, and Ref­er­er Check­ing.

If you are using a CDN such as Cloud­Front to speed up your site’s deliv­ery of con­tent, val­i­dat­ing the Referer head­er at the web serv­er becomes less prac­ti­cal. The CDN stores a copy of your con­tent in the edge of its net­work of servers, so even if your web serv­er val­i­dates the orig­i­nal request’s head­ers (in this case, the Referer), addi­tion­al requests for that con­tent must be val­i­dat­ed by the CDN itself because they are unlike­ly to hit the ori­gin web server.

Ulti­mate­ly, a ref­er­er head­er would not work any­way for my sit­u­a­tion. Since the fonts and the images exist on the same Cloud­Front serv­er, I need­ed to add an addi­tion­al rule in AWS.

AWS WAF

What is WAF? Ama­zon’s FAQ:

AWS WAF is a web appli­ca­tion fire­wall that helps pro­tect web appli­ca­tions from attacks by allow­ing you to con­fig­ure rules that allow, block, or mon­i­tor (count) web requests based on con­di­tions that you define. These con­di­tions include IP address­es, HTTP head­ers, HTTP body, URI strings, SQL injec­tion and cross-site scripting.

Head to the WAF start page and cre­ate a rule, which is called an ACL, for access con­trol list.” I’ve cre­at­ed one called jmx2-ref­er­er-check-for-fonts.

2021 07 20 13 58 46

There is a handy GUI to cre­ate your rules. I’ll talk through the steps I took, but since the rules can be writ­ten in JSON, that’s what I’ll include below for ref­er­ence. The lan­guage I’ll use is pret­ty stilt­ed, but I think it’s the best way to describe using the GUI

Under the Add rules” drop­down, I select­ed Add my own rules and rule groups.” 

I chose to make a reg­u­lar rule” and not a rate-based rule”. If a request match­es all the state­ments, which I’ll describe next, then block and send a cus­tom head­er with a 403 response code with the error mes­sage of Invalid request for access to font”.

For the 2 state­ments, the first was a negat­ed” state­ment that inspects the head­er in the ref­er­er” field name with the match type of con­tains string” with the string to match of jmx2​.com”. I trans­formed the text to lowercase.

The sec­ond state­ment is where I checked for the font. This state was not negat­ed. It inspects the URI path to see if it con­tains the string woff”. The text is trans­formed to lowercase.

That means if I had an image with the string woff”, it would be caught in this rule. That’s a down­side I’ll live with though. 

This rule is applied to the Cloud­Front dis­tri­b­u­tion I’ve got for the site. Now images can be served to any loca­tion, but my fonts look for jmx2​.com in their ref­er­er header. 

Here’s the JSON for the rule.

{
  "Name": "jmx2-referer-check-for-fonts",
  "Priority": 0,
  "Action": {
    "Block": {
      "CustomResponse": {
        "ResponseHeaders": [
          {
            "Name": "Error",
            "Value": "Invalid request for access to font"
          }
        ],
        "ResponseCode": 403
      }
    }
  },
  "VisibilityConfig": {
    "SampledRequestsEnabled": true,
    "CloudWatchMetricsEnabled": true,
    "MetricName": "jmx2-referer-check"
  },
  "Statement": {
    "AndStatement": {
      "Statements": [
        {
          "NotStatement": {
            "Statement": {
              "ByteMatchStatement": {
                "FieldToMatch": {
                  "SingleHeader": {
                    "Name": "referer"
                  }
                },
                "PositionalConstraint": "CONTAINS",
                "SearchString": "jmx2.com",
                "TextTransformations": [
                  {
                    "Type": "LOWERCASE",
                    "Priority": 0
                  }
                ]
              }
            }
          }
        },
        {
          "ByteMatchStatement": {
            "FieldToMatch": {
              "UriPath": {}
            },
            "PositionalConstraint": "CONTAINS",
            "SearchString": "woff",
            "TextTransformations": [
              {
                "Type": "LOWERCASE",
                "Priority": 0
              }
            ]
          }
        }
      ]
    }
  }
}

The cost of pro­tec­tion is not free

Now my fonts are pro­tect­ed but there is a cost asso­ci­at­ed with AWS WAF. It’s $5 per web ACL per month and $1 per rule per month and $0.60 per mil­lion requests. 

What I’ve described above is overkill for my com­pa­ny site. It would be cheap­er not to have these rules in place or fig­ure out a dif­fer­ent host­ing solu­tion for the fonts. I’ve set the rules up pri­mar­i­ly to exper­i­ment on some­thing that is not client work. I’m doing it for the experience.