I now run a small self-hosted watchtower that pings me on Telegram the moment something looks wrong on any Laravel app I manage. It didn't start as a plan. It started with a friend's text message and a server that had already been compromised for who knows how long.
The app nobody thought was a target
The app was PaketLebaranku.id — a small Laravel + MySQL + Livewire platform running on a modest VPS through Biznet Gio, built for a friend's small business. Its only job: track installment savings for people paying into a Lebaran package program, small recurring deposits that add up to something meaningful once a year.
It's exactly the kind of app that never shows up in anyone's threat model. No enterprise budget, no dedicated ops team, no security monitoring — just a business owner and a friend who built the thing on the side. Which, as it turned out, is precisely why it got hit.
"The app is throwing a 500 error, can you check?"
That was the whole message. I expected a stack trace, maybe a bad migration or an expired dependency. Instead, digging into the error turned up something far worse.
The .env file wasn't misconfigured — it was gone. Any Laravel developer knows what that means: database credentials, app keys, third-party secrets, either already exposed or already pulled by whoever got in.
Then a second find: a handful of backdoor scripts sitting inside public/icons, dressed up to look like they belonged there. Then a third: .htaccess had been rewritten to let Python scripts execute inside a directory that should never be able to run anything.
Three separate footholds, sitting quietly in an app tied to real people's savings, for who knows how long before a user happened to hit an error page.
The gap that actually mattered
Once we'd contained it, the obvious question was "how do I catch this faster next time." But sitting with it longer, I realized the real failure wasn't a missing check — it was that nothing was watching at all. A scan run once, manually, after the fact doesn't help. The incident wasn't caught by a security tool; it was caught by luck, in the form of an annoyed user.
That reframed what I actually needed to build: not just something that could find this class of problem, but something that would look for it without anyone having to remember to ask.
Laravel Scalpel: the checks I wish had already existed
The first piece is Laravel Scalpel, a narrow, fast forensic scanner for Laravel codebases. It doesn't try to be a full SAST suite — it answers the exact three questions that would have shortened this incident from days to minutes: are there files sitting where they shouldn't be, does the environment configuration look right, are there permission or execution paths that shouldn't exist.
It's picked up some traction beyond my own use case — featured by the Laravel Artisan Community, 24+ GitHub stars, and a pull request currently open to get it listed in awesome-laravel.
N8N Bastion: making sure someone's actually watching
The second piece, N8N Bastion, is what closes the real gap. It's a self-monitoring stack built on n8n that turns infrastructure and application problems into a Telegram message — instead of a support ticket that arrives after the damage is done. Laravel Scalpel now runs inside it as a scheduled check, not a tool anyone has to remember to invoke.
Together they cover both halves of the problem: Scalpel knows what to look for on a Laravel app, and Bastion makes sure it's actually looking, on a schedule, without a human in the loop.
Why this is worth open-sourcing
PaketLebaranku.id will never make a breach headline. It's too small, too obscure, too ordinary — and that's exactly the point. There are a lot of small businesses running real, sensitive workloads on a single self-managed VPS with no monitoring at all, not because the owners don't care, but because every security tool out there quietly assumes a team and a budget they don't have.
Laravel Scalpel and N8N Bastion exist because of one real incident on one real app. I'm putting both out there because I'd bet a lot of other small Laravel apps are sitting exactly where PaketLebaranku.id was, right before that 500 error showed up.
If you run a Laravel app on a VPS you manage yourself, I'd genuinely like to know what breaks or what's missing if you try these:
- Laravel Scalpel: https://github.com/hryagstn/laravel-scalpel
- N8N Bastion: https://github.com/hryagstn/n8n-bastion
Top comments (0)