A modern e-commerce site is a little code you write and a lot of code you import. The storefront, its API, the Magento backend, the serverless functions, the infrastructure: every repository pulls in hundreds of packages, and any of them can publish a well-known vulnerability tomorrow.
We already had a scanner. It ran every day, found vulnerabilities and opened tickets. And that is where it all stopped: detection fixes nothing. A ticket waits until a developer has time, looks up the right version, opens the merge request, reruns CI. Meanwhile, the fix waits.
On 23 September, we replaced that chain with a loop that fixes things itself: self-hosted Renovate opening the fixes, and a daily scan checking what is still exposed. Here is the method, because our customers’ security depends on it too.
The principle: a merge request is a signal
Renovate is known for opening a merge request for every new version of every dependency. Used that way across all our repositories, it would bury a team of three developers in updates, and security fixes would get lost in the noise.
We went the other way: every update is disabled by default. Only vulnerability alerts from the OSV database re-enable a package. When a vulnerability is published on a dependency we use, a merge request opens right away, suffixed [SECURITY]. If a merge request shows up, there is a reason to deal with it.
packageRules: [
{
description: 'Security updates only: vulnerability alerts re-enable the affected packages',
matchPackageNames: ['*'],
enabled: false,
},
// ...
],
osvVulnerabilityAlerts: true,
The whole policy fits in a single configuration file, in a dedicated project. No repository carries a Renovate file: changing a rule is one merge request, reviewed in one place, and it applies everywhere.
Renovate does not run as a service. It has no server and no database: every run is a scheduled CI job that starts, works through each repository, and stops.
The four-hour loop
Every four hours, three sources can open a merge request.
Direct dependencies. OSV alerts cover npm, Composer, Maven and PyPI, plus the Composer versions pinned in our Dockerfiles. A vulnerability published in the morning has its fix proposed within four hours.
Indirect dependencies. This is the classic blind spot: alerts only see packages declared directly, while a vulnerability often hides three levels down. A job of our own handles it after every Renovate run. It scans each repository, keeps the serious vulnerabilities in indirect dependencies that have a published fix, and upgrades only those packages, within the versions their parents already accept, touching nothing but the lockfile. Then it scans again. It only opens its merge request if vulnerabilities disappeared and none appeared. When the fix lies outside the versions the parent accepts, it forces nothing: it reports it, and a human decides whether to upgrade the parent.
Monday refreshes. Some versions have no vulnerability feed at all: the base images in our Dockerfiles, tools installed by version, the lockfiles themselves. Every Monday before 6 a.m., Renovate groups them into one merge request per repository. Images stay on their major version, PHP and Python on their minor version: changing a major is a migration, not a refresh. These updates only offer versions published at least three days earlier, giving the registry time to spot and pull a compromised package.
To get a version pinned in a Dockerfile tracked, a comment above it is all it takes:
# renovate: datasource=github-releases depName=helm/helm
ENV HELM_VERSION="v4.3.0"
# renovate: datasource=github-releases depName=jkroepke/helm-secrets extractVersion=^v(?<version>.*)$
ARG HELM_SECRETS_VERSION=4.7.5
Who gets to merge on its own
That is the question that worries people, rightly so. Our answer comes down to a few strict rules.
Three repositories only: the storefront, the backend and the in-store app. Those are the ones where a merge goes to preproduction first, and production remains a manual release, after acceptance testing. In the other repositories, where a merge goes straight to production, everything goes through a human.
Patch and minor only. Major versions always wait for a person: they may require code changes.
A green pipeline, mandatory. Renovate merges on its own, on the next run, once the merge request pipeline is green: tests, build, ephemeral environment. We disabled the auto-merge delegated to GitLab, which merges a merge request with no pipeline immediately when asked to merge “when the pipeline succeeds”. A branch without a pipeline is never merged automatically.
A code review for every security fix. Our review agent reviews every [SECURITY] merge request and sets a status on the commit. Below 80/100, nothing merges. To avoid triggering a new review on every run, Renovate only rebases a branch when it has a conflict.
Not on Fridays, not at night. Automatic merges only happen Monday to Thursday, between 9 a.m. and 5 p.m. If something breaks in preproduction, someone is there to see it.
Never the Magento core or a package we patch. Our backend applies Composer patches to some packages. An update can let a patch still apply without error while changing its meaning, and no test would notice. Before every run, a script reads the list of patched packages from composer.json. If it cannot read it, no backend update merges on its own anymore: the rule fails on the safe side.
Finally, Renovate opens at most five merge requests at a time and two per hour. Our pipelines share runners with deployments, and a wave of updates must not delay a release.
The daily check
Renovate fixes what it knows how to fix. What is still exposed has to be verified, and that is the daily scan’s job.
Every morning at 6:30, Trivy analyses:
- the main branch of every repository, which is the code in production: production dependencies and secrets committed by mistake;
- Helm charts and Kubernetes manifests, for critical misconfigurations;
- 25 container images, public and private. Private images are read through a read-only AWS role that only trusts the project’s main-branch pipelines, over OIDC, with no stored key.
It is the only dependency scan across all our repositories. None of them runs its own: one place, one threshold, one alert channel.
A finding has only three possible outcomes.
Fix it. Most of the time, Renovate’s merge request is already open: all that is left is to merge it.
Rebuild. An OS vulnerability in one of our base images triggers a rebuild with the distribution’s latest packages. The next day’s scan confirms the fix.
Accept it, in writing and for a limited time. Sometimes no upstream fix exists yet, or it requires a migration that has to be planned. The risk is then accepted through a merge request on the project, with a reason and an expiry date:
vulnerabilities:
- id: CVE-YYYY-NNNNN
statement: "Fix only available in the next major, migration planned"
expired_at: 2026-11-30
The project’s CI rejects any acceptance without a date. An acceptance without a date would be permanent, and a permanent acceptance is just something forgotten. On expiry, the finding turns the scan red again, until it is fixed or a new explicit decision is made.
What already existed followed the same rule from day one: a short, dated acceptance, after which every remaining finding becomes an alert again.
Watching the watcher
A security system that stops without warning is worse than no system at all: it gives false assurance. So a watchdog runs every day and turns red if:
- no Renovate run has succeeded in the last 24 hours;
- a security merge request has been open for more than three business days;
- the review of a security fix has been pending for more than an hour.
Every action of the system is announced in a dedicated Slack channel: merge request opened, merged automatically, rebuild started, scan finding, failed job with its cause. Nobody has to go looking for information.
One last detail that matters: the schedules belong to a service account, not a person. They keep running when someone goes on holiday, changes teams or sees their token expire.
Two weeks later
As of 7 October, 37 merge requests for security or maintenance have been merged across five repositories. Some were merged with no human involved, with a green pipeline and an approved review. For the rest, a developer’s work comes down to reviewing and merging a merge request that is already prepared, instead of starting from a ticket.
The loop also hit its own limits, which we fixed along the way: tools pinned in two places that had to move together, connectors in our integration bus with no vulnerability feed at all, which now get a weekly refresh, and base images that are now rebuilt automatically as soon as an OS vulnerability is found in them.
What we learned
- Detection is not enough. A vulnerability ticket is one more task. A merge request ready to merge is a five-minute decision.
- Fewer merge requests, handled better. Disabling every update except security made every merge request legitimate.
- Automation has to be earned. Auto-merge only exists where a preproduction stage and a manual release protect production, and only for minor updates that are tested and reviewed.
- Accepting a risk is a decision, so it has a date. An exception with no deadline becomes a forgotten vulnerability.
- Watch the watcher. A security tool that stops silently is the worst-case scenario. The watchdog matters as much as the scan.
Dependency security is not a project you finish. It is a loop that runs every four hours, and our job is to make sure it never stops.