Back to Blog

Language: English

A Security Hardening Roundup

Hardening measures added in parallel around the same audit window: secretlint in pre-push, a single OAuth-based deletion request path, resolving a vulnerable transitive dependency, and making CI output fail-closed.

Around the same audit as the incident where the WAF configuration got rolled back (A Refactor Had Rolled Back the WAF Configuration), several security improvements went in in parallel. Each one is small in isolation, but the kind that becomes a hole if left unattended. Recording them together as a roundup.

Adding secretlint to pre-push

Credential leak checking previously consisted only of detecting private key files. Generic secrets like API keys, tokens, and service account keys were a blank spot.

I put secretlint into pre-push.

Right before push rather than on every commit, because push is the boundary with the outside world. Running on every commit feels bad day to day and motivates people to disable it.

The repository contains dummy credentials for development. Excluding them wholesale would gut the check, so I allowed them individually via the relevant sub-rules’ allows. Global exclusions are limited to build artifacts and dependencies.

Where in the push flow it runs looks like this:

git push
  -> pre-push hook
  -> secretlint がリポジトリ全体を検査する
  -> 検出したら push を中断する

Unifying the Deletion Request Path

The deletion request path for banned users was unified into a single route: re-authentication with a Google OAuth ID token.

The token’s signature, audience, issuer, expiry, and email_verified are verified, and the Firebase UID is resolved server-side from the verified email.

With the path unified into one, the flow looks like this:

削除申請
  -> Google OAuth の ID token を受け取る
  -> 署名 / audience / issuer / 有効期限 / email_verified を検証する
  -> 検証済みの email から Firebase の UID を解決する
  -> 申請を受け付ける

A single path that never proceeds to accepting the request until every verification passes.

Four things were removed: the old endpoint, stored Firebase tokens, silent fallbacks, and unreachable UI states. Never storing Google tokens in Web Storage is now fixed as a contract too.

The requirement: a banned user must not be able to act using tokens acquired before the ban.

Dependencies and Output

A transitive dependency contained a Critical vulnerability, so I bumped it to the patched version. This application doesn’t actively exercise the affected path, but the package is in the production dependency graph. The bump fit within the parent package’s allowed range, so no direct dependency or override was used. I didn’t want to add permanent resolution policies or manifest merging.

CI output got attention too. When a Terraform plan failed, stdout and stderr originating from the PR were displayed as-is. What appears there depends entirely on the PR contents — unknowable in advance. I replaced it with a fixed status, and root-causing now queries Cloud Audit Logs with a fixed filter. Only four approved items are emitted, classified fail-closed.

Checks We Didn’t Have

Introducing secretlint, updating dependencies, unifying the auth path — all of it was just doing what hadn’t been done. The kind of work where doing it strictly leaves things better off.