In Depth: Securing Apps on Forge

[InDepth#21] I've had this question many times, so let me take you through the steps I follow when provisioning and securing apps on Forge.

In Depth: Securing Apps on Forge

TL;DR: I’ve provided a checklist at the end which lists each of my steps. 🙂

I get asked a lot about how I secure my own Laravel apps and how I deploy them through services like Laravel Forge1, so I thought it’d be a good topic to cover in an In Depth. So I wrote up the first draft and it hit the Email Limit, so I’ve had to cut it back a bit. This month we’re going to cover the very basics2 of setting up a Laravel app with secure defaults, and then work through the steps I take when deploying to Laravel Forge.

Next month, I’ll follow this up with another In Depth that focuses specifically on Laravel and the different tools and configurations I commonly use. This will include things like Telescope, Horizon, auth scaffolding, etc. (If you’d like me to include anything specific in here, let me know!)

Now, before we dive into it, I want to acknowledge that I work as a solo developer on the apps I manage, and I also manage them in my spare time3. This is reflected in some of my decisions, and if you manage a large app with multiple servers and a dev team, you may need to tweak some of my recommendations to fit your needs.

Configuring Laravel

First up, let’s look at what I do first when creating a new Laravel project.

This is usually done through the Composer create-project command:

composer create-project laravel/laravel example

1. Commit Fresh Install

The very first thing I do is commit the fresh install to git. This is useful from a security point of view, as it means you’ll notice if any packages you install initially modify your app code - which is something a malicious package may do. It’s also useful from a non-security point of view, as it lets you easily rollback changes in case you make a mistake.

valorin@Eowyn:~/dev/example$ git init
Initialized empty Git repository in /home/valorin/dev/example/.git/
valorin@Eowyn:~/dev/example (master #%)$ git checkout -b main
Switched to a new branch 'main'
valorin@Eowyn:~/dev/example (main #%)$ git add .
valorin@Eowyn:~/dev/example (main +)$ git commit -m "Fresh Laravel installation"
[main (root-commit) eb24b7d] Fresh Laravel installation
 79 files changed, 11128 insertions(+)

Now that this is set up, I commit each of the changes we’re working through separately. This allows me to track each step and rollback as needed.

2. Update .env.example to Production Values

The next thing I do is update .env.example to have production values for as much as I can configure. At a bare minimum, this means setting the name, environment, debug mode, and URL, and usually tweaking the different connections and drivers too. Some of these won’t be known yet, but I toggle the ones that are known.

APP_NAME="Securing Laravel"
The above changes to the .env.example file in a git diff.

The reason I do this is pretty straightforward: you copy this file as a template in production when setting up the app, so why not start with a production version? You’re much less likely to deploy to production with debug values enabled if your template doesn’t have debug values available. (But make sure you don’t put any production secrets in here!)

That said, this approach might not work when you need to share your local dev config between devs using .env.example, but in those cases you could look at using naming like .env.production and .env.local instead, so it’s obvious what template applies to each.

Also, if you have a your own build and deploy system, maybe drop .env.example entirely and have your .env provisioned from a secure secrets store.

3. Inject Canary Tokens