Dec 4, 2018 5 min read
Trusted Proxies and Middleware as a Firewall Replacement
Manager, Digital Product Delivery
We use a Laravel-powered dashboard app at Highland to monitor the health of the company. The app updates all its data using scheduled tasks, so aside from consuming that data, we don’t interact with the app. We’ve no need for user accounts except to keep the info private, but creating accounts and logging in feels excessive, so we decided to lock things down another way.
Our first solution was to set up a firewall, but when team members started building Slack apps and Alexa skills that query the dashboard’s API, we knew we needed to manage whitelisted IPs at a higher-level to more easily account for volatile IPs. We opted for a fairly simple solution powered by middleware and Trusted Proxies (a package Laravel includes by default) for the front end and, of course, Passport for the API.
Implementing the solution
If you clicked the above link to Laravel’s docs, you’ll notice they tell you to just drop your proxies in
app/Http/Middleware/TrustProxies.php all willy nilly, but that doesn’t always play out so well across different environments. Inspecting the extended
Fideloper\Proxy\TrustProxies class for an unintrusive alternative, I discovered it pulls from the config in its method
setTrustedProxyIpAddresses() which the oh-so-familiar
handle() method (or, dare I say, gentleman-caller) calls for us.
I could just drop a new config variable in
config/app.php and use it in
app/Http/Middleware/TrustProxies.php, but Trusted Proxies already has one, so it feels a little more out-of-the-box to just use it.
I only need to run
php artisan vendor:publish —-provider="Fideloper\Proxy\TrustedProxyServiceProvider" so I can drop an environment variable in there.
Side note: I name my environment variables after the related file and config key, e.g.
proxies key. This standard prevents naming collisions and keeps teams from getting frustrated over Jimmy’s HORRIBLE NAMING CHOICES, which in turn saves time during retros. #protip
I ran into some fun⸮ issues and discovered that specifying my proxies as an array works on the server, but using
* (WHITELIST ALL THE IPS! _o/) in an array was problematic when running tests on Travis. That explains the unsightly string vs. array war at the top of my
Trusted Proxies doesn’t actually block IPs. It just happens to overlap cleanly enough for my taste to repurpose things a bit. To block any IPs not whitelisted as trusted proxies, we need some simple middleware. So simple I won’t even explain it. Here it is:
When registering the middleware, I didn’t want to wrap everything in
routes/web.php in a single
Route::group(), so I just added it to the web middleware group in
app/Http/Kernel.php to keep from muddying things up.
Potentially relevant to some… our dashboard’s front end is an SPA that uses API-querying Vue components, and worrying about bearer tokens when dogfooding an API on the same server is unnecessarily tedious nonsense. So I haz another middleware for you to point your lookyballs at. It suffices for our uses, but definitely not all, so don’t use it blindly.
This checks if the request is from one of the whitelisted IPs and injects our bearer token if so. When getting the bearer token, it pulls from the cache if already set, otherwise it deletes the existing token(s) (just to keep the database a bit more manageable) then creates and caches a new one. Of course, I also need to assign the middleware a key in
$routeMiddleware. I choose you, Pika-
I also need to set the middleware priority to ensure the app calls the
InjectBearerTokenForLocalApiRequests middleware early enough that the app doesn’t reject the requests. The
App\Http\Kernel class extends
Illuminate\Foundation\Http\Kernel which already has the
$middlewarePriority property defined. I copied that over to
app/Http/Kernel.php and dropped my middleware in there as late in priority as practical.
Since not all of the dashboard’s API endpoints are relevant, I opted to
Route::group() the relevant jabronis in
With this in place:
we can remove the firewall,
my team members can query the API (with Passport securing that side), and
Bob’s your uncle (assuming you have an Uncle Bob… I do not).
Let me know if you have any interest in seeing some tests or would benefit from a writeup on working with Projector’s API, which, for the uninitiated, is a bit less straightforward than we typically expect from modern apps.