kits / secure-skills
Secure Skills
Secret scanning, dependency audits, CSP rollout and first-hour incident steps, with configs ready to copy into your repo.
- version
- 1.0.0
- files
- 22
- licence
- MIT
- pairs with
- kittybin Dev
What’s inside
secure-skills/ ├── CHANGELOG.md ├── LICENSE.txt ├── README.md ├── checklists/auth-and-sessions.md ├── checklists/file-uploads.md ├── dependencies/README.md ├── dependencies/dependency-audit.yml ├── dependencies/update-policy.md ├── dependencies/scripts/audit-deps.sh ├── headers/csp-rollout.md ├── headers/headers-checklist.md ├── headers/csp-report-endpoint/node-server.mjs ├── headers/csp-report-endpoint/worker.js ├── incident/evidence-log.md ├── incident/first-hour-checklist.md ├── secrets/.gitleaks.toml ├── secrets/.pre-commit-config.yaml ├── secrets/README.md ├── secrets/leaked-secret-response.md ├── secrets/pre-commit-setup.md ├── secrets/secret-scan.yml └── secrets/hooks/pre-commit
Preview
secrets/README.mdpreview
Secret scanning
A secret scanner reads your code and git history and flags anything that looks like a key, token, password or private key. This folder sets one up at three layers, so a leak has to get past all of them:
- On your machine, before a commit is created (pre-commit hook).
- In CI, on every pull request and every push to your main branch.
- In your git host, using its own secret scanning and push protection where available.
The files use gitleaks, an open-source scanner (MIT licence) that runs as a single binary on Windows, macOS and Linux. The same ideas apply to other scanners if your team already uses one.
Files in this folder
| File | What it is |
|---|---|
| `.gitleaks.toml` | Config that keeps the built-in rules, adds one custom rule and shows how to allowlist known-safe values |
| `pre-commit-setup.md` | Two ways to run the scanner before each commit, and how to roll it out to a team |
| `.pre-commit-config.yaml` | Config for the pre-commit framework |
| `hooks/pre-commit` | A plain git hook for teams that don't use the pre-commit framework |
| `secret-scan.yml` | GitHub Actions workflow that scans pull requests, pushes to main and the full history once a week |
| `leaked-secret-response.md` | What to do when the scanner, or anyone else, finds a real secret |
Set it up in four steps
- Install gitleaks on your machine. Use your package manager (
brew install gitleaks,scoop install gitleaks,winget install gitleaks) or download a release from the gitleaks GitHub releases page. - Copy
.gitleaks.tomlto the root of your repository. gitleaks picks it up automatically from there. - Run a first full scan of history, with secrets redacted from the output:
gitleaks git --redact --verbose .
Deal with every finding before you go further. A finding in old history is still a live secret until it is rotated. Follow leaked-secret-response.md.
- Add the pre-commit hook (
pre-commit-setup.md) and copysecret-scan.ymlto.github/workflows/secret-scan.yml.
Older gitleaks releases use gitleaks detect instead of gitleaks git, and gitleaks protect --staged instead of gitleaks git --pre-commit --staged. If a command in this kit says "unknown command", upgrade gitleaks rather than changing the files.
How the config works
.gitleaks.toml has three parts.
[extend] useDefault = true keeps the built-in rule set. It covers the common cloud providers, payment platforms, chat tools, private keys and generic high-entropy tokens. Don't remove this line unless you plan to maintain every rule yourself.
[[rules]] adds a rule for a token format that only your company uses. The example matches tokens shaped like exco_live_ followed by 32 letters and digits. Change the id, description, regex and keywords to match your own format. keywords is a fast pre-filter: gitleaks only runs the regex on lines that contain one of them, so keep at least one short, distinctive keyword.
[[allowlists]] tells gitleaks what to ignore. The example shows the four kinds of entry:
| Field | What it ignores | Example use |
|---|---|---|
| `paths` | Whole files, matched by regex against the path | `.env.example`, test fixtures, lockfiles |
| `regexes` | Findings whose matched secret fits the regex | Obvious placeholders such as `EXAMPLE_KEY` or a key made of zeros |
| `stopwords` | Findings whose matched secret contains the word | `placeholder`, `changeme`, `dummy` |
| `commits` | Every finding in a given commit | A commit that added a secret which has since been rotated |
Older gitleaks releases only understand a single [allowlist] table. If yours rejects [[allowlists]], upgrade, or rename the header to [allowlist] and keep the same fields.
How the CI workflow works
secret-scan.yml runs in three situations:
| Trigger | What it scans | Why |
|---|---|---|
| Pull request | Only the commits in the pull request | Fast feedback, and a finding blocks the merge |
| Push to `main` | Full history | Catches anything merged without a pull request |
| Weekly schedule (Monday 05:23 UTC) | Full history | Picks up new built-in rules after you upgrade gitleaks |
Notes on the choices:
- It downloads a pinned gitleaks release and checks it against the published checksums file before running it. Raise
GITLEAKS_VERSIONwhen you upgrade locally, so CI and laptops use the same rules. - It runs the binary directly instead of a marketplace action. Some third-party actions need a licence key for organisation accounts; the binary doesn't.
--redactkeeps secrets out of the CI log. Without it, the scanner would print the very secret it found into a log that many people can read.permissions: contents: readgives the job the least access it needs.- To make the scan block merges, add the
gitleaksjob as a required status check in your branch protection rules. - If your default branch isn't
main, change thebranchesline.
Rules for the allowlist
The allowlist is where scanners quietly stop working. Keep it small and reviewed.
- Allowlist values or files, never whole folders of real code.
- Never allowlist a real secret because "it's only the test key". Test keys still give access to your account, and test environments often hold copies of real data.
- Add a
commitsentry only after the secret in that commit has been rotated and the old one revoked. - Review changes to
.gitleaks.tomlin pull requests like any security change. Consider a CODEOWNERS entry for it.
Inline ignores
gitleaks also skips a line that ends with the marker gitleaks:allow inside a comment in that file. It is quick, but it hides the decision inside code where nobody reviews it. Prefer an allowlist entry, which keeps every exception in one file.
A baseline for old findings
If a large, old repository has many findings you have already dealt with, you can record them in a baseline and only fail on new ones:
gitleaks git --report-path gitleaks-baseline.json .
gitleaks git --baseline-path gitleaks-baseline.json --redact .
Only do this after every finding in the baseline has been rotated or confirmed as a false positive. A baseline of unreviewed findings is a list of live keys that you have agreed to stop looking at.
Turn on your git host's scanning too
- GitHub: secret scanning and push protection are on by default for public repositories. For private repositories they depend on your plan. Check Settings, then "Code security".
- GitLab: secret detection runs as a CI template, and recent versions offer push protection.
- Bitbucket: has secret scanning for repositories in Bitbucket Cloud.
Host scanning often knows about partner token formats and can alert the provider, who may revoke the key for you. It doesn't replace local scanning: by the time it runs, the secret has already left your machine.
What scanning can't do
- It finds patterns. A password with no recognisable shape, typed into a config file, can slip through.
- It can't see secrets that never touch git: screenshots, chat messages, logs, browser bundles built in CI.
- It can't undo a leak. Once a secret has been pushed anywhere public, treat it as used and rotate it.
headers/csp-rollout.mdpreview
Content Security Policy rollout
A Content Security Policy (CSP) tells the browser which scripts, styles, frames and connections a page may use. A good one turns most cross-site scripting bugs from "attacker runs code in your users' browsers" into "browser blocks it and sends you a report". A careless one breaks your checkout on a Friday evening.
This guide takes you from no policy to an enforced one in five stages, with reports collected the whole way.
Stage 1: collect reports before you write a policy
You need somewhere for browsers to send violation reports. Use csp-report-endpoint/ in this folder, or a hosted reporting service.
| Option | When to use it |
|---|---|
| `csp-report-endpoint/worker.js` | You use Cloudflare Workers or a similar edge runtime. Deploy it on a route such as `https://example.com/csp-report` |
| `csp-report-endpoint/node-server.mjs` | You run Node.js (version 18 or later). Run it behind your reverse proxy on the same route |
| A hosted reporting service | You want dashboards and grouping without running anything |
Both endpoint files:
- accept the older
report-uriformat and the newer Reporting API (report-to) format - reject bodies over 64 KB and anything that isn't JSON
- strip query strings and fragments from URLs before logging, because those often carry tokens and personal data
- write one JSON line per violation to the log, which most log tools can search and group
- answer CORS preflight requests only for origins you list in
ALLOWED_ORIGINS
Settings (environment variables for Node, variables or secrets for the worker):
| Variable | Example | Meaning |
|---|---|---|
| `ALLOWED_ORIGINS` | `https://example.com,https://www.example.com` | Sites allowed to send reports cross-origin. Not needed if the endpoint is on the same origin as the pages |
| `REPORT_PATH` | `/csp-report` | Path the endpoint listens on |
| `PORT` | `8787` | Node only. Port to listen on |
Run the Node version with node node-server.mjs. It has no dependencies.
Reports can be sent by anyone, so treat the data as untrusted. Add a rate limit at your proxy or edge if the endpoint is public, and don't display report contents in an admin page without escaping them.
Stage 2: deploy a report-only policy
Content-Security-Policy-Report-Only makes the browser report violations without blocking anything. Start with a strict policy in this mode so the reports show you everything that would break.
Reporting-Endpoints: csp="https://example.com/csp-report"
Content-Security-Policy-Report-Only: default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; font-src 'self'; connect-src 'self'; frame-src 'none'; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'; report-uri https://example.com/csp-report; report-to csp
Include both report-uri and report-to. Browsers that support report-to ignore report-uri; older ones only understand report-uri.
Leave it running for at least a week, or one full release cycle, so you see traffic from every page and every browser your users have.
Stage 3: read the reports and decide
Group reports by directive and blockedUrl. For each group, decide one of three things:
| Report looks like | Likely cause | Action |
|---|---|---|
| `script-src` with a third-party domain you use (payments, analytics, chat widget) | Expected script | Add that exact host to `script-src` |
| `script-src` with `blockedUrl` of `inline` | Inline `<script>` or `onclick=` handler | Move to a file, or use a nonce or hash (stage 4) |
| `script-src` with `eval` | Code calling `eval` or `new Function` | Find the library. Replace it if you can; add `'unsafe-eval'` only as a last resort |
| `style-src` with `inline` | Inline `style=` attributes or `<style>` blocks | Nonce or hash for `<style>` blocks. Many apps accept `'unsafe-inline'` in `style-src`; it is far less dangerous than in `script-src` |
| `connect-src` with your API or a provider | `fetch`, XHR or WebSocket call | Add the host. Include `wss://` hosts for realtime features |
| `img-src` with `blob:` or `data:` | Generated images, previews | Add `blob:` or `data:` to `img-src` only |
| `frame-src` with a payment or video host | Embedded iframe | Add that exact host to `frame-src` |
| A domain you don't recognise, from a single user | Browser extension or injected malware on that user's machine | Ignore it. Don't loosen the policy for one person's extension |
| `chrome-extension`, `moz-extension`, `safari-web-extension` | Browser extension | Ignore it |
Keep a short list of every source you add and why. When a provider is removed from your app, remove its host from the policy too.
Stage 4: replace inline scripts with nonces or hashes
Allowing 'unsafe-inline' in script-src removes most of the protection a CSP gives you. There are two clean alternatives.
Nonce (best for server-rendered pages)
The server creates a fresh random value for every response, puts it in the header and adds it to each <script> tag it renders. An attacker who injects a script can't guess the value.
Content-Security-Policy: script-src 'nonce-R4nd0mValueFromServer' 'strict-dynamic'; object-src 'none'; base-uri 'none'
<script nonce="R4nd0mValueFromServer" src="/app.js"></script>
Rules for nonces:
- At least 128 bits of randomness from a secure random generator, base64 encoded. In Node:
crypto.randomBytes(16).toString("base64"). - New value for every response. A nonce in a cached page is the same for everyone, which defeats the point. Don't let a CDN cache HTML that carries a nonce, or move to hashes.
- Never echo a nonce from the request, and never put it in a URL.
'strict-dynamic'lets a trusted (nonced) script load further scripts, which is how most bundlers and tag managers work. With it, host lists inscript-srcare ignored by modern browsers.
A minimal Express example:
import crypto from "node:crypto";
app.use((req, res, next) => {
const nonce = crypto.randomBytes(16).toString("base64");
res.locals.cspNonce = nonce;
res.setHeader(
"Content-Security-Policy",
`script-src 'nonce-${nonce}' 'strict-dynamic'; object-src 'none'; base-uri 'none'; frame-ancestors 'none'`
);
next();
});
Then render nonce="<%= cspNonce %>" (or your template's equivalent) on every script tag. Next.js, Nuxt, SvelteKit, Rails and Django all have documented ways to do this; use your framework's method rather than rolling your own.
Hash (best for static sites)
For a fixed inline script, put the SHA-256 hash of its exact contents in the policy. Static hosts can't create a nonce per response, so hashes are the usual choice there.
printf '%s' 'document.documentElement.classList.add("js")' | openssl dgst -sha256 -binary | openssl base64
Content-Security-Policy: script-src 'self' 'sha256-<the base64 output>'
The hash covers the exact characters between <script> and </script>, including spaces and line breaks. Any change, even reformatting, needs a new hash. Your browser's console prints the expected hash when it blocks an inline script, which is the quickest way to get it right.
Hashes don't cover onclick= and other inline event handlers. Move those into a script file with addEventListener.
Stage 5: enforce, and keep reporting
- When a week of reports shows only noise (extensions, single users), copy the report-only policy to a
Content-Security-Policyheader. - Keep the report-only header too, with a stricter candidate policy. This lets you test the next tightening without risk.
- Keep
report-uriandreport-toon the enforced policy, so you hear about breakage straight away. - Watch reports closely for the first day after each release.
Common directives
| Directive | Controls | Starter value |
|---|---|---|
| `default-src` | Fallback for most fetch directives not listed | `'self'` |
| `script-src` | JavaScript files and inline scripts | `'self'`, or a nonce with `'strict-dynamic'` |
| `style-src` | Stylesheets and inline styles | `'self'` (add `'unsafe-inline'` only if needed) |
| `img-src` | Images and favicons | `'self' data:` |
| `font-src` | Web fonts | `'self'` plus your font host |
| `connect-src` | `fetch`, XHR, WebSocket, EventSource | `'self'` plus your API and provider hosts |
| `frame-src` | What your page may embed in iframes | `'none'` or exact hosts |
| `frame-ancestors` | Who may embed your page in an iframe | `'none'` (replaces `X-Frame-Options`) |
| `object-src` | Plugins such as `<object>` and `<embed>` | `'none'` |
| `base-uri` | Allowed values for `<base href>` | `'self'` or `'none'` |
| `form-action` | Where forms may submit | `'self'` plus your payment host if forms post there |
| `worker-src` | Web workers and service workers | `'self'` |
| `manifest-src` | Web app manifest | `'self'` |
| `media-src` | Audio and video | `'self'` plus your media host |
| `upgrade-insecure-requests` | Rewrites `http:` subresource URLs to `https:` | Include it on HTTPS-only sites |
| `report-uri` / `report-to` | Where violation reports go | Your reporting endpoint |
Notes:
frame-ancestors,report-uriandreport-todon't work in a<meta http-equiv>CSP. Send the policy as a header whenever you can.'self'doesn't include subdomains.https://cdn.example.commust be listed separately.- Avoid wide sources such as
https:,*or a whole CDN domain that hosts anyone's files. Each one lets an attacker load code from somewhere they control. - Keep policies short enough to read. If yours is very long, you probably have third-party scripts that need review.
Trusted Types (next step)
Once your CSP is enforced, require-trusted-types-for 'script' makes the browser reject strings passed to dangerous DOM sinks such as innerHTML, unless they go through a policy you define. It blocks most DOM-based cross-site scripting. Browser support is not universal yet, so roll it out in report-only mode first, exactly as above.
Rollout record
| Date | Change | Mode | Who | Result |
|---|---|---|---|---|
| [Date] | Strict starter policy | Report-only | [Name] | [Notes] |
| | | | | |
| | | | | |incident/first-hour-checklist.mdpreview
First hour: suspected breach or leaked key
Use this when you think someone has accessed something they shouldn't, or a credential with real power has been exposed. Print it, or keep it somewhere you can open when your main systems might not be trustworthy.
The first hour has three goals, in this order:
- Stop it getting worse (contain).
- Keep the evidence you will need to understand what happened.
- Tell the right people early, with facts.
Fixing the root cause and writing the full report come later.
Incident started (UTC): [time] Noticed by: [name] How noticed: [alert, user report, provider email, scanner]
Incident lead: [name] Notes kept by: [name] Notes location: [document or channel]
---
Minutes 0 to 10: confirm and organise
- One person takes the lead. Everyone else reports to them, so actions don't collide.
- Start a written log now (see
evidence-log.md). Record every action with a UTC time. - Write one sentence on what you know: "At [time] we saw [what] affecting [which system]."
- Open a private channel or call for the response. Don't discuss it in public channels, or in any channel the attacker might be able to read if an account is compromised.
- Decide the working severity. Leaked admin or cloud credentials, signs of data access, or signs of an attacker inside your systems all count as the highest level until proven otherwise.
Minutes 10 to 30: contain
Contain first, investigate second. But contain in a way that keeps evidence (next section).
- Revoke or rotate the exposed credential (see "Key rotation order" below). For a confirmed leak, this is the first action.
- Disable compromised accounts. Suspend rather than delete, so their history and audit trail survive.
- End active sessions for affected accounts, and for all admins if an admin account is involved.
- Block the access path if you know it: an IP range at the firewall or edge, an API key, an OAuth app, a public storage link.
- Isolate affected machines from the network rather than shutting them down. Shutting down loses memory contents and running processes.
- Pause risky automation that could spread the problem: deploy pipelines, sync jobs, scheduled exports.
- Put the affected feature in maintenance mode if data is actively being taken and you can't stop it any other way.
Don't:
- Wipe, reimage or restore over affected systems yet.
- Delete the attacker's files, accounts or keys before recording them.
- Rotate credentials from a machine that might be compromised. Use a known-clean device.
- Contact the attacker, or reply to an extortion message, without legal advice.
Minutes 10 to 40: keep evidence (in parallel)
Logs expire, and some providers keep them for only a few days. Export first, read later.
- Export audit logs from the affected providers: cloud console, git host, identity provider, database, payment platform, email.
- Export web server, CDN, WAF and application logs covering at least the 7 days before the first sign of trouble.
- Take snapshots of affected cloud disks and databases before changing them.
- Save copies of any attacker artefacts: files, scripts, new users, new keys, changed settings. Record names, IDs and timestamps.
- Screenshot dashboards that show the activity (usage spikes, unusual regions, new resources).
- Save the original alert, email or report that started this, with full headers.
- Store all of this in one access-controlled place, separate from the affected systems. Note who collected each item and when.
- Extend log retention on affected services if you can.
Minutes 20 to 60: tell people
Tell people early with what you know, and say clearly what you don't know yet.
| Who | When | What they need |
|---|---|---|
| Your manager or founder | Straight away | What happened, what you've done, what you need |
| Security lead, if you have one | Straight away | Full details and access to the log |
| Engineering lead for the affected system | Straight away | What to stop doing, what to check |
| Legal or privacy contact | Within the hour if personal data may be involved | Facts so far, so they can start the clock on notification duties |
| Your cloud or SaaS provider's security team | When you see misuse on their platform | Account ID, times, what you observed |
| Insurer, if you have cyber cover | Early. Policies often require prompt notice and may provide a response firm | Policy number, summary |
| Customer support | Before customers start asking | A holding line to use, and who to escalate to |
| Customers and regulators | After legal advice, unless a deadline forces earlier action | Handled by your legal or privacy contact |
Data protection laws can set short deadlines. Under the GDPR, for example, a reportable personal data breach must be notified to the supervisory authority within 72 hours of becoming aware of it. Other regions and sectors have their own rules. This kit can't tell you what applies to you; that is why the legal or privacy contact is in the first hour.
Key rotation order
Rotate in order of power: the credentials that could be used to create new access or undo your other rotations go first.
- Identity and cloud administration. Cloud root or owner accounts, organisation admin accounts, identity provider admins, domain registrar and DNS accounts. Change passwords, reset MFA if there is any doubt, remove unknown MFA devices and recovery options.
- CI/CD and source control. Deploy keys, CI secrets, git host tokens and personal access tokens, package registry publish tokens. These can ship new code to production and to your users.
- Secrets managers and key stores. The master credentials that unlock all the others.
- Databases and storage. Database passwords and connection strings, storage access keys, backup credentials.
- Signing secrets. Session and JWT signing keys, OAuth client secrets, webhook signing secrets, encryption keys. Rotating session keys logs everyone out; accept that.
- Payment and money-moving keys. Payment provider secret keys, payout and banking API credentials.
- Third-party APIs. Email, SMS, maps, analytics, AI and other service keys, especially anything with a spending meter.
- Everything else that the compromised system or person could read.
For each one:
- Create the new credential with the narrowest scope that works.
- Update every place it is used.
- Deploy and confirm things work.
- Revoke the old one, and record the time.
- Look for credentials the attacker created (new keys, tokens, users, OAuth grants, SSH keys) and remove them after recording them.
End of the first hour
- The exposed credentials are revoked, or there is a written plan with a time for each.
- Evidence is exported and stored safely.
- The right people know, and the next update time is set.
- There is a list of open questions: how they got in, what they touched, whether they still have access.
- Someone owns each open question, with a time to report back.
- You have decided whether you need outside help (an incident response firm, your provider's security team, law enforcement).
Questions to answer next
These usually take hours to days. Write each answer with the evidence behind it.
- How did they get in, and is that path closed?
- When did it start? (The first sign is rarely the first event.)
- What did they access, change or take?
- Do they still have any access: accounts, keys, scheduled jobs, backdoors?
- Whose data was involved, and how many people?
- What do we need to tell users, partners and regulators, and by when?
Contacts
| Role | Name | Phone | Email | Backup |
|---|---|---|---|---|
| Incident lead | | | | |
| Security | | | | |
| Engineering lead | | | | |
| Legal or privacy | | | | |
| Communications | | | | |
| Cloud provider support | | | | |
| Insurer | | | | |
| Incident response firm | | | | |