{"course":{"slug":"cloud-and-shipping","title":"Shipping It","url":"https://addaly.com/learn/cloud-and-shipping","level":"intermediate","track":"build"},"lesson":{"slug":"the-security-you-actually-need","title":"The short list that actually matters","url":"https://addaly.com/learn/cloud-and-shipping/the-security-you-actually-need","position":40,"of":74,"minutes":10,"answer":"Small sites are compromised through leaked credentials, exposed services, missing authorisation checks and unpatched dependencies, so spend your effort there before anywhere else.","keyIdea":"Small sites are compromised through leaked credentials, exposed services, missing authorisation checks and unpatched dependencies, so spend your effort there before anywhere else."},"body":{"format":"markdown","text":"## Nobody is attacking you personally\n\nThat is the useful frame. Almost nothing that happens to a small site is targeted. It is automated, indiscriminate, and looking for a short list of known-easy things. Which means a short list of defences covers most of the risk, and the exotic material you read about is not where your effort belongs yet.\n\n## The machine\n\nIf you run your own virtual machine:\n\n- **SSH keys only.** `PasswordAuthentication no`, `PermitRootLogin no`. Then look at the logs a week after launch: `journalctl -u ssh | grep -c \"Failed password\"` on a fresh public box regularly runs to thousands per day. That is the ambient rate, arriving at every address on the internet.\n- **A firewall with three ports open**: 22, 80, 443. Everything else closed by default.\n- **Your database must not have a public IP.** \"It has a password\" is not a plan. Shodan and similar services index open Postgres, Redis and MongoDB instances continuously, and unauthenticated Redis in particular has been mass-exploited for years. Bind to a private network, or to localhost, and reach it through the application or an SSH tunnel.\n- **Automatic security updates.** `unattended-upgrades` on Debian or Ubuntu. It is one package and it closes the window on the kernel and OpenSSL issues you will otherwise never patch.\n\n## The accounts\n\nThis is where the highest-value five minutes live, and it is not code.\n\nMulti-factor authentication on: your **domain registrar**, your **DNS provider**, your **email**, your **cloud account**, your **GitHub**. The registrar and the email are the crown jewels: with control of your email, an attacker resets everything else; with control of your DNS, they receive your traffic and can issue a valid certificate for your domain.\n\nUse a password manager, use unique passwords, and store the MFA recovery codes somewhere a second person can reach them. Locking yourself out is a more likely outcome than being attacked.\n\n## The dependencies, and an honest word about the noise\n\nTurn on **Dependabot** or **Renovate**, both free. Run `npm audit` or your language's equivalent in CI.\n\nThe honest part, which most advice omits: a large fraction of CVE alerts in a lockfile are for code paths you never execute: a vulnerability in the dev server of a build tool, or in a function your dependency does not call. If you treat every alert as urgent you will spend your week on it, and then you will start ignoring the feed, and then you will miss the one that matters.\n\nSo triage. Is this package in the running application, or only in the build? Is the vulnerable function reachable from your code? Is it remotely triggerable? Fix the reachable ones promptly, batch the rest monthly, and keep updating regularly so no single update is frightening.\n\n## The application, in four items\n\n**Parameterised queries, always.** SQL injection is decades old and still near the top of every breach report, and it is still string concatenation. Your ORM does this for you until the moment somebody builds a `WHERE` clause with a template literal.\n\n**Authorisation checked per object, on the server.** The most common real bug in small applications is not injection; it is `/invoice/1234` where changing the number to 1235 shows somebody else's invoice. Every handler that loads a record by ID must check that this session is allowed that record. Hiding the link in the UI is not a check.\n\n**Cookies set properly.** `HttpOnly` so JavaScript cannot read the session, `Secure` so it is never sent over HTTP, `SameSite=Lax` so other sites cannot make authenticated requests on the user's behalf.\n\n**A rate-limited login**, from the previous module, plus a check of submitted passwords against known-breached lists. Credential stuffing is the highest-volume real attack on a small site.\n\n## Least privilege for the database user\n\nThe account your application connects with does not need to create tables, drop them, or read other databases. Give it `SELECT`, `INSERT`, `UPDATE`, `DELETE` on the tables it uses. Migrations run as a different, more powerful role, in a job you control.\n\nThis costs ten minutes and converts several categories of injection from catastrophe into incident.\n\n## Headers, and the one with teeth\n\nMost security headers are one line each and worth adding. `Content-Security-Policy` is the one that actually prevents cross-site scripting, and the one people get wrong, because a strict policy breaks inline scripts and third-party embeds.\n\nDeploy it in report-only mode first (`Content-Security-Policy-Report-Only`), collect violations for a week, then enforce. Going straight to enforcement on a live site breaks your own analytics tag on a Friday afternoon.\n\n## Free tools, all of them\n\n`ssh-audit` for your SSH configuration, `testssl.sh` for your TLS, **OWASP ZAP** for a basic application scan, and Mozilla's Observatory for headers. None of them cost anything, and running each once before launch is an afternoon that puts you ahead of most sites on the internet."},"check":{"question":"You have limited time before launch. Which of these gives the largest reduction in real risk for a small public site?","options":[{"text":"A strict Content-Security-Policy, enforced from day one","correct":false,"tempting":"Valuable and worth doing, but it defends against one class of bug, and enforced without a report-only period it is most likely to break your own site."},{"text":"Multi-factor authentication on the registrar, DNS, email and cloud accounts, and no public IP on the database","correct":true},{"text":"An automated vulnerability scanner running weekly against the application","correct":false,"tempting":"Produces a report, mostly of low-severity findings, and does nothing about the credential and exposure paths that account for most real compromises."},{"text":"Resolving every dependency alert reported by `npm audit` before launch","correct":false,"tempting":"Consumes the most time for the least benefit, since most alerts are unreachable from your code, and it delays the launch without closing the likely doors."}]},"prev":{"slug":"backups-and-restore","title":"The restore you have never tested","url":"https://addaly.com/learn/cloud-and-shipping/backups-and-restore"},"next":{"slug":"data-you-must-delete","title":"Data you must keep, and data you must delete","url":"https://addaly.com/learn/cloud-and-shipping/data-you-must-delete"},"authorship":"Founder and series editor: Dr Mustafa Mun. Written by AI models to a written standard. Exam keys checked blind by a second model: 3,665 of 3,666 agreed. Academic review invited.","review":{"status":"Draft, open for academic review"},"cite":"Addaly (2026) 'The short list that actually matters', in Shipping It. Edited by M. Mun. Addaly. Available at: https://addaly.com/learn/cloud-and-shipping/the-security-you-actually-need (Accessed: 13 October 2026).","licence":{"name":"CC BY-NC-SA 4.0","url":"https://creativecommons.org/licenses/by-nc-sa/4.0/"},"reader":{"schema":1,"title":"The short list that actually matters","courseTitle":"Shipping It","module":{"index":5,"slug":"staying-alive","title":"Staying alive: the bad day and the day after"},"answer":null,"keyIdea":"Small sites are compromised through leaked credentials, exposed services, missing authorisation checks and unpatched dependencies, so spend your effort there before anywhere else.","hero":{"kind":"plate","image":null,"alt":null,"caption":null},"blocks":[{"t":"h2","id":"nobody-is-attacking-you-personally","runs":[{"text":"Nobody is attacking you personally"}]},{"t":"p","runs":[{"text":"That is the useful frame. Almost nothing that happens to a small site is targeted. It is automated, indiscriminate, and looking for a short list of known-easy things. Which means a short list of defences covers most of the risk, and the exotic material you read about is not where your effort belongs yet."}]},{"t":"h2","id":"the-machine","runs":[{"text":"The machine"}]},{"t":"p","runs":[{"text":"If you run your own virtual machine:"}]},{"t":"ul","items":[[{"text":"SSH keys only.","b":true},{"text":" "},{"text":"PasswordAuthentication no","code":true},{"text":", "},{"text":"PermitRootLogin no","code":true},{"text":". Then look at the logs a week after launch: "},{"text":"journalctl -u ssh | grep -c \"Failed password\"","code":true},{"text":" on a fresh public box regularly runs to thousands per day. That is the ambient rate, arriving at every address on the internet."}],[{"text":"A firewall with three ports open","b":true},{"text":": 22, 80, 443. Everything else closed by default."}],[{"text":"Your database must not have a public IP.","b":true},{"text":" “It has a password” is not a plan. Shodan and similar services index open Postgres, Redis and MongoDB instances continuously, and unauthenticated Redis in particular has been mass-exploited for years. Bind to a private network, or to localhost, and reach it through the application or an SSH tunnel."}],[{"text":"Automatic security updates.","b":true},{"text":" "},{"text":"unattended-upgrades","code":true},{"text":" on Debian or Ubuntu. It is one package and it closes the window on the kernel and OpenSSL issues you will otherwise never patch."}]]},{"t":"h2","id":"the-accounts","runs":[{"text":"The accounts"}]},{"t":"p","runs":[{"text":"This is where the highest-value five minutes live, and it is not code."}]},{"t":"p","runs":[{"text":"Multi-factor authentication on: your "},{"text":"domain registrar","b":true},{"text":", your "},{"text":"DNS provider","b":true},{"text":", your "},{"text":"email","b":true},{"text":", your "},{"text":"cloud account","b":true},{"text":", your "},{"text":"GitHub","b":true},{"text":". The registrar and the email are the crown jewels: with control of your email, an attacker resets everything else; with control of your DNS, they receive your traffic and can issue a valid certificate for your domain."}]},{"t":"p","runs":[{"text":"Use a password manager, use unique passwords, and store the MFA recovery codes somewhere a second person can reach them. Locking yourself out is a more likely outcome than being attacked."}]},{"t":"h2","id":"the-dependencies-and-an-honest-word-about-the-noise","runs":[{"text":"The dependencies, and an honest word about the noise"}]},{"t":"p","runs":[{"text":"Turn on "},{"text":"Dependabot","b":true},{"text":" or "},{"text":"Renovate","b":true},{"text":", both free. Run "},{"text":"npm audit","code":true},{"text":" or your language’s equivalent in CI."}]},{"t":"p","runs":[{"text":"The honest part, which most advice omits: a large fraction of CVE alerts in a lockfile are for code paths you never execute: a vulnerability in the dev server of a build tool, or in a function your dependency does not call. If you treat every alert as urgent you will spend your week on it, and then you will start ignoring the feed, and then you will miss the one that matters."}]},{"t":"p","runs":[{"text":"So triage. Is this package in the running application, or only in the build? Is the vulnerable function reachable from your code? Is it remotely triggerable? Fix the reachable ones promptly, batch the rest monthly, and keep updating regularly so no single update is frightening."}]},{"t":"h2","id":"the-application-in-four-items","runs":[{"text":"The application, in four items"}]},{"t":"p","runs":[{"text":"Parameterised queries, always.","b":true},{"text":" SQL injection is decades old and still near the top of every breach report, and it is still string concatenation. Your ORM does this for you until the moment somebody builds a "},{"text":"WHERE","code":true},{"text":" clause with a template literal."}]},{"t":"p","runs":[{"text":"Authorisation checked per object, on the server.","b":true},{"text":" The most common real bug in small applications is not injection; it is "},{"text":"/invoice/1234","code":true},{"text":" where changing the number to 1235 shows somebody else’s invoice. Every handler that loads a record by ID must check that this session is allowed that record. Hiding the link in the UI is not a check."}]},{"t":"p","runs":[{"text":"Cookies set properly.","b":true},{"text":" "},{"text":"HttpOnly","code":true},{"text":" so JavaScript cannot read the session, "},{"text":"Secure","code":true},{"text":" so it is never sent over HTTP, "},{"text":"SameSite=Lax","code":true},{"text":" so other sites cannot make authenticated requests on the user’s behalf."}]},{"t":"p","runs":[{"text":"A rate-limited login","b":true},{"text":", from the previous module, plus a check of submitted passwords against known-breached lists. Credential stuffing is the highest-volume real attack on a small site."}]},{"t":"h2","id":"least-privilege-for-the-database-user","runs":[{"text":"Least privilege for the database user"}]},{"t":"p","runs":[{"text":"The account your application connects with does not need to create tables, drop them, or read other databases. Give it "},{"text":"SELECT","code":true},{"text":", "},{"text":"INSERT","code":true},{"text":", "},{"text":"UPDATE","code":true},{"text":", "},{"text":"DELETE","code":true},{"text":" on the tables it uses. Migrations run as a different, more powerful role, in a job you control."}]},{"t":"p","runs":[{"text":"This costs ten minutes and converts several categories of injection from catastrophe into incident."}]},{"t":"h2","id":"headers-and-the-one-with-teeth","runs":[{"text":"Headers, and the one with teeth"}]},{"t":"p","runs":[{"text":"Most security headers are one line each and worth adding. "},{"text":"Content-Security-Policy","code":true},{"text":" is the one that actually prevents cross-site scripting, and the one people get wrong, because a strict policy breaks inline scripts and third-party embeds."}]},{"t":"p","runs":[{"text":"Deploy it in report-only mode first ("},{"text":"Content-Security-Policy-Report-Only","code":true},{"text":"), collect violations for a week, then enforce. Going straight to enforcement on a live site breaks your own analytics tag on a Friday afternoon."}]},{"t":"h2","id":"free-tools-all-of-them","runs":[{"text":"Free tools, all of them"}]},{"t":"p","runs":[{"text":"ssh-audit","code":true},{"text":" for your SSH configuration, "},{"text":"testssl.sh","code":true},{"text":" for your TLS, "},{"text":"OWASP ZAP","b":true},{"text":" for a basic application scan, and Mozilla’s Observatory for headers. None of them cost anything, and running each once before launch is an afternoon that puts you ahead of most sites on the internet."}]}],"check":{"question":[{"text":"You have limited time before launch. Which of these gives the largest reduction in real risk for a small public site?"}],"options":[{"text":[{"text":"A strict Content-Security-Policy, enforced from day one"}],"correct":false,"tempting":[{"text":"Valuable and worth doing, but it defends against one class of bug, and enforced without a report-only period it is most likely to break your own site."}]},{"text":[{"text":"Multi-factor authentication on the registrar, DNS, email and cloud accounts, and no public IP on the database"}],"correct":true,"tempting":null},{"text":[{"text":"An automated vulnerability scanner running weekly against the application"}],"correct":false,"tempting":[{"text":"Produces a report, mostly of low-severity findings, and does nothing about the credential and exposure paths that account for most real compromises."}]},{"text":[{"text":"Resolving every dependency alert reported by "},{"text":"npm audit","code":true},{"text":" before launch"}],"correct":false,"tempting":[{"text":"Consumes the most time for the least benefit, since most alerts are unreachable from your code, and it delays the launch without closing the likely doors."}]}],"order":[0,1,3,2],"why":null},"prev":{"slug":"backups-and-restore","title":"The restore you have never tested","minutes":8},"next":{"slug":"data-you-must-delete","title":"Data you must keep, and data you must delete","minutes":9}}}