You already know what has to be protected (lesson 01-01) and what from (lesson 01-02). The most important question remains: on what basis are decisions taken? Nobody can memorise the infinite list of correct configurations, and no such list would survive the next technology shift. What does survive are the principles: a handful of design rules, some of them formulated more than fifty years ago, that still explain why one architecture holds and another falls apart. In this lesson you are going to internalise those principles and see them applied to concrete Nimbus Reservas decisions: the permissions of the account the API uses to talk to PostgreSQL, the layers surrounding the attachments bucket, who may deploy to production. These principles are the backbone of the rest of the course: when we discuss hardening or cloud security in module 5, we will be applying what is here.
Contents
- Why principles and not recipes
- Least privilege and need to know
- Defence in depth
- Secure by default and security by design
- Fail-safe / fail-secure
- Separation of duties and rotation
- Complete mediation
- Economy of mechanism and least common mechanism
- Psychological acceptability
- Not relying on security through obscurity
- Zero Trust and the end of the perimeter
- Assume breach
- Summary table and the security-usability-cost trade-off
- Why principles and not recipes
Marta, the Nimbus CTO, receives contradictory proposals every week: a supplier sells her a web application firewall, an article says that what matters is MFA, a customer demands an 80-question questionnaire. Without a criterion of her own, security turns into a shopping list.
Most of the principles you will see here come from a 1975 paper by Saltzer and Schroeder on the protection of information in computer systems, later extended by industry practice. They have survived mainframes, client-server, the web and the cloud — and they still explain the incidents of 2026. That longevity is the reason to study them: a specific recommendation expires; a principle lets you generate the right recommendation for a technology that does not exist yet.
A warning before we start: the principles conflict with one another. Economy of mechanism calls for simplicity; defence in depth adds layers. Secure by default calls for restriction; psychological acceptability calls for not drowning the user. Applying them well is not about obeying all of them to the maximum, but about knowing which one weighs most in each decision and being able to explain why.
- Least privilege and need to know
2.1 Least privilege
Every entity — person, process or service — must have exactly the permissions it needs for its function, not one more, and only for as long as it needs them.
It is the most profitable principle of all, because it limits the damage of any failure, whether it comes from an attack, a mistake or a bug. If a compromised credential can only read three tables, the attacker only reads three tables.
Applied to Nimbus: the API's database account.
Iván started the project connecting the API with the postgres superuser, "so as not to fight with permissions". It works perfectly... until an SQL injection or a bug turns that convenience into a total disaster: with the superuser, an attacker can read any table, drop them all, create new users and, in some scenarios, run commands on the system.
This is how least privilege is applied in PostgreSQL:
-- === 1. A role with no log-in capability that groups the permissions ===
CREATE ROLE nimbus_api_role NOLOGIN;
-- === 2. Revoke what PostgreSQL grants by default ===
-- By default, any role can create objects in the public schema.
REVOKE ALL ON SCHEMA public FROM PUBLIC;
GRANT USAGE ON SCHEMA public TO nimbus_api_role; -- see the schema, not create in it
-- === 3. Grant ONLY what is needed, table by table and operation by operation ===
GRANT SELECT, INSERT, UPDATE ON bookings TO nimbus_api_role;
GRANT SELECT, INSERT, UPDATE ON end_customers TO nimbus_api_role;
GRANT SELECT ON services TO nimbus_api_role; -- catalogue: read only
GRANT SELECT, INSERT ON access_audit TO nimbus_api_role; -- appended to, NOT modified
GRANT USAGE ON ALL SEQUENCES IN SCHEMA public TO nimbus_api_role; -- for the auto-increments
-- === 4. Not a single table with DELETE or DROP ===
-- Removals are done with a logical delete (deleted_at column), not with DELETE.
-- === 5. Especially sensitive data: nothing by default ===
REVOKE ALL ON payroll FROM nimbus_api_role; -- the API never touches HR
REVOKE ALL ON gateway_keys FROM nimbus_api_role;
-- === 6. The application's real user inherits from the role ===
CREATE USER nimbus_api WITH PASSWORD :'generated_password';
GRANT nimbus_api_role TO nimbus_api;Explanation of the key decisions:
- Separating role from user (steps 1 and 6) lets you rotate the password or create a second user (for an asynchronous worker, say) without rewriting every
GRANT. - The
REVOKEin step 2 is essential. Many people grant permissions and assume everything else is closed; in PostgreSQL, thepublicschema is permissive by default. Least privilege starts by taking away, not by giving. access_auditwithINSERTbut withoutUPDATEorDELETE. This directly underpins the traceability and non-repudiation from 01-01: not even an attacker who gets hold of the API credential can erase their own trail from there.- No
DELETEon any table. ADELETE FROM bookingswith noWHEREstops being possible. Logical deletion also allows recovery without restoring backups. - An explicit
REVOKEonpayroll. Even if it had never been granted, writing it documents the intent and protects against a futureGRANT ... ON ALL TABLESdone in a hurry.
The same principle in the cloud, with the policy of the machine that runs the API:
# Object storage access policy for the API role
# Principle: only ITS prefix, only the actions it uses, only with encryption
Version: "2012-10-17"
Statement:
- Sid: ReadWriteTenantAttachmentsOnly
Effect: Allow
Action:
- "s3:GetObject"
- "s3:PutObject"
Resource: "arn:aws:s3:::nimbus-adjuntos-prod/tenants/*" # NOT the whole bucket
Condition:
StringEquals:
"s3:x-amz-server-side-encryption": "aws:kms" # forces encryption on write
- Sid: DenyDeletionAndBackupTampering
Effect: Deny # Deny always beats Allow
Action:
- "s3:DeleteObject"
- "s3:PutBucketPolicy"
- "s3:PutBucketAcl"
Resource:
- "arn:aws:s3:::nimbus-adjuntos-prod/*"
- "arn:aws:s3:::nimbus-backups-prod/*"Points worth highlighting:
- The resource is
/tenants/*, not*: the API cannot touch the bucket's internal configuration prefixes. - The encryption condition turns "remember to encrypt" into "it is impossible to write without encrypting".
- The
Denyblock includes the actions that would allow changing the rules of the game (PutBucketPolicy,PutBucketAcl). An attacker holding that credential cannot grant themselves more permissions. - The backups are explicitly out of the API's reach. That is the structural defence against ransomware: the compromised workload cannot reach the backup that saves you.
2.2 Need to know
This is the same principle applied to information rather than to operations: having the right authorisation level is not enough; you have to need that data for the specific task.
Rubén, in support, has general authorisation to look up end customer records. But he does not need to see every record of every Nimbus customer all the time. He needs to see the record of the customer with an open ticket.
| Without need to know | With need to know |
|---|---|
Rubén has permanent access to the whole end_customers table |
Access is opened for the tenant of the assigned ticket and is logged |
| The external consultancy has access to every environment | Access only to the environment of the incident, within a limited time window |
| The monthly board report includes identifying data | The report is generated in aggregate, with no personal identifiers |
In practice it is implemented with just-in-time access (granted when the ticket is opened and expiring when it is closed), field masking and aggregation. It is also the basis of the data minimisation principle required by data protection law (06-03).
- Defence in depth
No control is infallible. Place several independent controls in series, so that the failure of one is not the failure of the system.
The classic metaphor is the castle: moat, wall, courtyard, keep. The more honest metaphor is Swiss cheese: every layer has holes, but if the layers are independent, it is unlikely the holes will line up.
Applied to the most delicate Nimbus asset — the attachments bucket with scanned medical reports:
flowchart TD
ATK[External attacker] --> L1
L1["LAYER 1 - PERIMETER\nWAF and rate limiting\nHTTPS only, TLS 1.2+"] --> L2
L2["LAYER 2 - IDENTITY\nSession authentication\nMFA for internal accounts"] --> L3
L3["LAYER 3 - AUTHORISATION\nPermission + tenant ownership\nchecked on every request"] --> L4
L4["LAYER 4 - NETWORK\nThe bucket is not public\nAccess only from the VPC"] --> L5
L5["LAYER 5 - RESOURCE PERMISSIONS\nLeast privilege policy\nwith no DeleteObject"] --> L6
L6["LAYER 6 - DATA\nEncryption at rest with KMS\nSigned, expiring URLs"] --> L7
L7["LAYER 7 - DETECTION\nAccess log, alerts on\nanomalous download volume"] --> L8
L8["LAYER 8 - RECOVERY\nVersioning and immutable backups\nin another account"]
How to read this diagram. An attacker who gets past one layer runs into the next. And most importantly: if they exfiltrate data anyway, layers 7 and 8 still add value — Nimbus finds out what was taken and when (traceability), and it has not lost the information (recovery).
The condition that makes it work: independence. Ten layers that all depend on the same identity directory are not ten layers; they are one. When you design your layers, ask yourself: what single failure would bring several down at once? At Nimbus that single failure would be the cloud provider's administrator account: whoever holds it cancels layers 4, 5, 6 and 8 in one go. Hence that account deserves disproportionate protection (MFA with a hardware key, exceptional use only, alerts on every use).
Frequent mistakes when applying defence in depth:
- Stacking layers of the same type. Three different antivirus products are not defence in depth; they are redundancy of the same control.
- Using the existence of one layer to relax another. "Since we have a WAF, we don't need to validate input in the code." The WAF is a generic filter and can be evaded; validation in the code is what knows the business rules.
- Forgetting the detection layer. Many architectures have five prevention layers and none for detection. If nobody is looking, the breach lasts months.
- Secure by default and security by design
These are two sibling principles that are usually cited together (and that European data protection law sets out explicitly).
4.1 Secure by default
The out-of-the-box configuration must be the most restrictive that is reasonable. If the user touches nothing, they must end up protected.
The default configuration is the one that ends up in production in most installations. Any insecure default becomes, statistically, a mass vulnerability.
| Decision in Nimbus | Insecure default | Secure default |
|---|---|---|
| New user in the admin panel | Created with the admin role and permissions removed afterwards |
Created with no permissions; they are granted explicitly |
| New bucket created by Lucía | Inherits the account configuration | Infrastructure template with public access block and mandatory encryption |
| New endpoint added by Iván | Public unless the authentication decorator is added | Authentication mandatory unless an explicit exception is added to an allowlist |
| New customer signing up | All modules enabled | Only the module they bought |
| API logging | DEBUG level with full bodies |
INFO level with no personal data |
The third case deserves some code, because it is the one that prevents the silliest data leaks. Let us compare:
# === INSECURE-BY-DEFAULT PATTERN ===
# The endpoint is public unless somebody remembers to protect it.
@router.get("/api/v1/facturas")
async def list_invoices(db=Depends(get_db)): # <-- they forgot the user dependency
return await db.fetch_all("SELECT * FROM invoices")# === SECURE-BY-DEFAULT PATTERN ===
# Authentication is applied to the WHOLE router; forgetting it is impossible.
router = APIRouter(dependencies=[Depends(require_authenticated_user)])
# And the genuinely public routes are declared one by one, explicitly and visibly:
PUBLIC_ROUTES = {"/api/v1/salud", "/api/v1/estado-servicio"}
@app.middleware("http")
async def require_authentication(request: Request, call_next):
if request.url.path not in PUBLIC_ROUTES:
if not await valid_session(request):
return JSONResponse({"detail": "Not authenticated"}, status_code=401)
return await call_next(request)The conceptual difference is enormous. In the first pattern, security depends on every developer remembering every time. In the second, failing by omission results in denial, and opening something up requires a deliberate, reviewable act (adding a route to PUBLIC_ROUTES shows up in code review and draws attention). This is also fail-secure behaviour, which we cover in section 5.
4.2 Security by design
Security is built in when the system is conceived, not as a final review.
The cost of fixing a flaw grows brutally depending on the phase in which it is detected:
| When it is detected | Relative cost of fixing | Example in Nimbus |
|---|---|---|
| At design time | 1 | Deciding that every table carries tenant_id and that the filter is mandatory |
| While coding | ~5 | Adding the check to an endpoint during code review |
| In testing | ~15 | Finding in QA that three endpoints do not filter by tenant |
| In production | ~50 | Emergency patch, out-of-window deployment |
| After an incident | ~200+ | Forensic investigation, notification, customer loss, reputation |
A concrete example of a design decision at Nimbus: instead of trusting every query to add WHERE tenant_id = ..., row level security is enabled in PostgreSQL, so that isolation between customers is guaranteed by the database:
-- Multi-tenant isolation stops depending on nobody forgetting a WHERE
ALTER TABLE bookings ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON bookings
USING (tenant_id = current_setting('app.current_tenant')::int);
-- The API sets the session variable at the start of each request, after authenticating:
-- SET LOCAL app.current_tenant = '42';
-- From then on, a "SELECT * FROM bookings" only sees the rows of tenant 42.Why this is design and not a patch: a developer can no longer cause a cross-customer leak by forgetting a clause. The security property has moved to a layer where the mistake is impossible, instead of being policed at each of the hundreds of places where it could be made. A practical note: SET LOCAL inside the transaction is essential if a connection pool is used, so that the value does not leak into the next request.
- Fail-safe / fail-secure
When something fails — and it will fail — the system must end up in the safest possible state. The default decision when in doubt is to deny.
Saltzer and Schroeder's classic formulation is fail-safe defaults: access is based on explicit permission, not on the absence of prohibition.
Two variants need to be told apart:
| Variant | On failure it... | Prioritises | Example |
|---|---|---|---|
| Fail-secure | Closes / denies | Confidentiality and integrity | If the authorisation service does not respond, the API returns 403 |
| Fail-safe (physical safety) | Opens / releases | Human life, availability | Emergency doors unlock in the event of a fire |
In computer systems we almost always want fail-secure; in systems with lives at stake, fail-safe. And sometimes you have to choose deliberately.
The awkward case at Nimbus. The service that validates permissions stops responding. Two options:
# === OPTION A: fail-open (DANGEROUS as a default) ===
try:
allowed = await permission_service.check(current_user, action)
except TimeoutError:
allowed = True # "so as not to break the service"...
# ...an attacker who takes the permission service down
# gets full access. The failure is the key.
# === OPTION B: fail-secure (CORRECT in most cases) ===
try:
allowed = await permission_service.check(current_user, action)
except TimeoutError:
logger.error("event=permissions_unavailable actor=%s action=%s", current_user.id, action)
raise HTTPException(503, "Service temporarily unavailable")
# Access is denied, an alert is raised and degradation is visible.Option A has a devastating property: it turns a denial-of-service attack into a privilege escalation. The attacker no longer needs to steal credentials; saturating the permission service is enough.
That said, fail-secure applied without judgement also does harm. If Nimbus decides that any internal failure prevents booking, a problem in the mail sending service would leave clinics with no diary. The correct resolution is graduated degradation:
| Failing component | Decision | Rationale |
|---|---|---|
| Permission service | Deny everything (fail-secure) | Without authorisation you cannot operate safely |
| Transactional e-mail service | Continue, queue and warn | E-mail is incidental to the main operation |
| Payment gateway | Deny the charge operation | Never treat an unconfirmed payment as good |
| Audit service | Deny on sensitive operations | With no record there is no traceability and no non-repudiation |
That last case is debatable and is useful for practising the reasoning: if a mass export of personal data cannot be logged, is it allowed to go ahead blind? At Nimbus, the agreed answer is no.
- Separation of duties and rotation
6.1 Separation of duties
No single person should control every phase of a critical process on their own.
It is a principle born in accounting audit and transplanted into computing with excellent results. It protects against deliberate fraud and against individual error at the same time.
Applied to Nimbus:
| Critical process | Without separation (risk) | With separation |
|---|---|---|
| Deployment to production | Iván writes, approves and deploys his own code | Iván writes; somebody else approves the PR; the deployment is executed by the pipeline, not by a person |
| Payments to suppliers | Sara creates the supplier and orders the payment | Sara registers the supplier; the payment order is authorised by the board |
| Access management | Lucía grants herself administrator permissions when she needs them | The grant requires a ticket approved by Marta and is recorded |
| Review of the audit logs | Lucía administers the systems and reviews her own records | The records go to a separate account where Lucía has no delete permission |
And this is how it materialises in the repository configuration, which is where separation of duties really lives in a software company:
# .github/branch-protection (simplified representation of the branch protection)
branch: main
required_pull_request_reviews:
required_approving_review_count: 1
dismiss_stale_reviews: true # a new push invalidates the previous approval
require_code_owner_reviews: true # sensitive files require a specific reviewer
required_status_checks:
strict: true
contexts:
- "tests"
- "security-static-analysis"
- "dependency-scanning"
enforce_admins: true # the rule applies to administrators TOO
allow_force_pushes: false # history cannot be rewritten
allow_deletions: falseExplanation of the lines that matter most:
enforce_admins: trueis the one almost everybody leaves asfalse. If Marta and Lucía can bypass the process, the process does not exist: compromising one of those two accounts is enough.dismiss_stale_reviews: trueprevents the trick of obtaining approval with an innocuous change and adding the real code afterwards.allow_force_pushes: falseprotects the historical integrity of the repository: nobody can erase the trace of a change.
A practical limit in an SME. Nimbus has a single systems administrator. Strict separation is impossible: Lucía has to be able to do her job at three in the morning. When preventive separation is not viable, it is replaced by compensating controls: an unalterable record of all her actions in an account she cannot write to, automatic alerts to Marta on critical actions, and periodic review by a third party. It is an honest answer and perfectly defensible in front of an auditor.
6.2 Rotation
This complements the above: people change roles and credentials expire.
- Rotation of people: having somebody else take over the function reveals irregularities and removes the dependency on one individual (the so-called bus factor). Mandatory holidays are, historically, an anti-fraud control.
- Rotation of credentials: API keys, certificates and secrets with a limited life. A credential that expires every 90 days bounds the useful window of a leak. Better still: short-lived credentials issued by the identity provider, valid for minutes, which eliminate the long-lived secret altogether.
- Complete mediation
Every access to every resource must be checked, every time. No exceptions, no shortcuts, no caches that outlive a permission change.
This principle attacks a very specific mistake: checking permissions once at the start and trusting from then on.
Examples of incomplete mediation at Nimbus:
- The SPA hides the "Export customers" button if the user lacks the permission... but the endpoint does not check it. The interface is not a security control: anybody can call the API directly.
- Rubén's session token lasts 8 hours. His permissions are revoked at 10:00, but his token remains valid, with the old permissions baked in, until 18:00.
- Authorisation is checked when listing bookings, but not when downloading the attachment of a specific booking: the file URL, if known, works without verification.
How the attachment case is implemented correctly:
@router.get("/api/v1/reservas/{booking_id}/adjunto")
async def download_attachment(booking_id: int, current_user=Depends(get_current_user), db=Depends(get_db)):
# COMPLETE MEDIATION: it is checked again here, even though it was checked when listing
booking = await db.fetch_one(
"SELECT attachment_key FROM bookings WHERE id=:id AND tenant_id=:t",
{"id": booking_id, "t": current_user.tenant_id},
)
if booking is None:
raise HTTPException(404, "Not found")
# The object's permanent URL is never exposed: a signed, expiring one is generated
url = generate_signed_url(
key=booking["attachment_key"],
expiry_seconds=120, # two minutes: enough to download
read_only=True,
)
logger.info("event=attachment_download actor_id=%s resource=booking:%s", current_user.id, booking_id)
return {"url": url}The three defensible decisions here: ownership is rechecked even though "it was already checked earlier"; the signed URL expires in two minutes, so sharing it by mistake has a minimal window; and the access is recorded, which makes the mediation auditable as well.
The tension in this principle is obvious: always checking costs performance. It is resolved with very short-lived caches and with explicit invalidation when permissions change — never by removing the check.
- Economy of mechanism and least common mechanism
8.1 Economy of mechanism (simplicity)
The security mechanism must be as small and simple as possible, because only the simple can be verified.
A permission system with 4 clear, auditable roles protects better in practice than one with 60 granular permissions that nobody understands and that get granted wholesale "because we don't know which one is missing". Complexity is not an aesthetic inconvenience: it is where flaws hide.
Signs that Nimbus is violating this principle:
- Nobody can explain in one sentence why a particular user can perform a particular action.
- There are three different authorisation mechanisms coexisting (one in the proxy, another in the middleware, another inside each endpoint) and it is not clear which one wins.
- Onboarding an employee means touching seven places, and there is a 20-page document that almost nobody follows.
Rule of thumb: if you cannot draw your authorisation model on a whiteboard in five minutes, you cannot guarantee it is correct.
8.2 Least common mechanism
Minimise the resources and mechanisms shared between users or between different systems, because everything shared is a potential channel for leakage or propagation.
Applied to Nimbus:
| Shared (risk) | Separated (better) |
|---|---|
| Production and pre-production in the same cloud account and the same VPC | Separate accounts; a pre-production compromise does not reach production |
| A single credential used by the API, the batch jobs and Lucía's scripts | One credential per identity, with different permissions and traceable |
| Real data copied into the test environment | Anonymised or synthetic data in testing |
| The same wifi for employees, guests and the printers | Separate networks; the guest one with no access to the internal one |
The case of pre-production with real data is especially instructive: test environments have fewer controls, less monitoring and more people with access. If they contain real data, you have created a copy of your most sensitive asset in your least protected environment — and, in data protection terms, a processing activity that is hard to justify.
- Psychological acceptability
If a control is too inconvenient, people work around it. A control that is worked around does not protect: worse, it hides the real exposure.
This principle, formulated back in 1975, remains the most ignored. Security that does not take people into account systematically produces the opposite of what it seeks:
| Badly designed control | Real behaviour it causes | Acceptable alternative |
|---|---|---|
| Password change every 30 days with 5 rules | Nimbus2026!, Nimbus2026!!, a sticky note under the keyboard |
Long passwords with no forced expiry + MFA + corporate password manager |
| Blocking every file transfer service | Rubén uses his personal account to send the CSV to the customer | A convenient corporate service, with expiry and logging |
| Access request with 3 approvals and a 5-day wait | Colleagues share credentials "in the meantime" | Self-service just-in-time access, with one person's approval and logging |
| MFA on every action, every 10 minutes | People look for ways to disable it; they approve prompts without reading them | MFA at log-in and on sensitive actions; sessions of a reasonable length |
The operational rule: make the secure path the easy path. If the corporate password manager is more convenient than a notepad, it will get used. If the internal transfer service is faster than WeTransfer, it will get used. Designing this way takes more work up front, but it is the only way for a control to survive past its second month.
A diagnostic warning: if you discover that people at Nimbus are bypassing a control, the first hypothesis should not be "they are irresponsible" but "the control is badly designed". It almost always is. This approach connects with the training and awareness lesson (06-05).
- Not relying on security through obscurity
The security of a system must not depend on its design being secret. It must depend on its keys being secret.
This is Kerckhoffs's principle, formulated in the nineteenth century for military cryptography and valid for any system: the design may fall into enemy hands without that compromising security.
What counts as illegitimate obscurity (it does not protect):
- Putting the admin panel at
/panel-secreto-nimbus-2024and not requiring strong authentication. It is found by an automated path scan. - Encrypting with a home-grown algorithm "that nobody knows". Home-made algorithms are almost always broken; the standards are public precisely because they have withstood decades of analysis.
- Trusting that a booking identifier is long and hard to guess instead of checking authorisation.
- Hiding the server version in the headers as the only measure against a known unpatched vulnerability.
What is legitimate (and is not the same thing):
- Not publishing internal details unnecessarily. Reducing the information you give away for free is useful as an additional layer; it simply cannot be your only layer.
- Secrets that are designed to be secret: cryptographic keys, passwords, tokens. There, secrecy is not obscurity: it is the mechanism.
- The key difference: a legitimate secret can be rotated if it leaks. If your security depends on nobody knowing the path to your panel, when it leaks you will have to redesign; if it depends on a key, you change the key.
A practical corollary for Nimbus: when Marta evaluates a security tool, she should be wary of a supplier that refuses to explain how it works. "It's proprietary, we can't give details" is not an acceptable answer about a cryptographic mechanism.
- Zero Trust and the end of the perimeter
11.1 Why the perimeter died
The classic model divided the world in two: outside (dangerous) and inside (trusted). A firewall at the border and that was that. At Nimbus, that model is simply inapplicable:
- Half the headcount works from home. Where is the "inside"?
- The servers are at a cloud provider, not in the office.
- An external consultancy connects remotely with elevated privileges.
- The end users' mobile app connects from any network in the world.
- The data lives in an S3 bucket and in third-party SaaS services.
On top of that, the perimeter model has a structural flaw: once you are inside, there is nothing more. An attacker who compromises a laptop moves laterally without resistance — which is exactly what modern ransomware does.
11.2 The principles of Zero Trust
Never trust, always verify. Location on the network grants no privileges.
flowchart TB
subgraph PER["PERIMETER MODEL (obsolete)"]
direction LR
FW[Firewall] --> INT["Internal network\nEVERYTHING trusts EVERYTHING"]
end
subgraph ZT["ZERO TRUST MODEL"]
direction LR
U[User or service] --> V{"Verification on every access\nidentity + device +\ncontext + least privilege"}
V -->|allowed, short session| REC[Specific resource]
V -->|denied| X[Block and log]
end
Zero Trust rests on three ideas you already know, combined:
- Explicit verification on every access (complete mediation) using every available signal: who you are, from which device, in what state that device is, at what time, from where.
- Least privilege with just-in-time access and short sessions.
- Assume breach: microsegmentation, end-to-end encryption and continual analysis, so that a compromise stays contained.
What it means concretely at Nimbus:
| Before (perimeter) | With Zero Trust |
|---|---|
| "If you are on the VPN, you can reach the database" | Access to the DB requires a verified identity, a managed device and an approved time window |
| The API trusts requests coming from the internal network | Services authenticate to each other with mTLS or service tokens |
| The consultancy's laptop joins the whole internal network | Access only to the specific system of the incident, with session recording |
| Containers in the same VPC talk to each other freely | Network policies that only allow the declared flows |
An important warning: Zero Trust is not a product you buy. It is an architecture and a journey of several years. For Nimbus, getting started is perfectly realistic without a big investment: removing implicit network-based trust from database access, requiring MFA on all administrative accounts and narrowing the consultancy's access are already three Zero Trust steps.
- Assume breach
Design on the basis that the attacker is already inside. The question is not "how do I stop them?", but "how much damage can they do and how long before I find out?".
It is a change of mindset more than a control. It changes the questions asked in architecture meetings:
| Preventive-mindset question | "Assume breach" question |
|---|---|
| How do I stop them getting in? | If they get in through Rubén's laptop, what can they reach from there? |
| Is the database protected? | If they steal the database backup, is the data encrypted and with which key? |
| Do we have backups? | Can the attacker who controls the infrastructure delete those backups? |
| Do we have logs? | How long would it take us to detect a slow, sustained exfiltration? |
| Is our CI/CD secure? | If a GitHub Actions token is compromised, what can it deploy and who approves it? |
The three most important design consequences:
- Segmentation: so that the compromise of one component does not grant access to the next.
- Backups out of reach: immutable backups or backups in another account, so that production credentials cannot destroy them. It is the difference between paying a ransom and not paying it.
- Detection and response: measuring the time to detection. An organisation that detects in hours suffers an incident; one that detects in months suffers a catastrophe.
This approach is developed operationally in the incident response plan (04-05) and in the monitoring techniques (05-02).
- Summary table and the security-usability-cost trade-off
13.1 The principles at a glance
| Principle | What it prevents | Concrete decision in Nimbus |
|---|---|---|
| Least privilege | A failure turning into total compromise | The nimbus_api account has no DELETE and no access to payroll |
| Need to know | Unnecessary exposure of data | Rubén sees the record of the open ticket, not the whole table |
| Defence in depth | A single failure bringing the system down | 8 layers around the attachments bucket |
| Secure by default | Insecure configurations reaching production | Router with mandatory authentication; public routes on an allowlist |
| Security by design | Expensive, late fixes | Multi-tenant isolation in the database (RLS), not in every query |
| Fail-secure | A failure opening the doors | If the permission service does not respond, access is denied and an alert is raised |
| Separation of duties | Uncontrolled fraud and error | enforce_admins: true; nobody approves their own deployment |
| Rotation | Eternal credentials and personal dependencies | Keys with expiry; short-lived credentials |
| Complete mediation | Permissions checked "once and done" | Ownership is rechecked on every attachment download |
| Economy of mechanism | Complexity that hides flaws | Four clear roles instead of sixty loose permissions |
| Least common mechanism | Propagation between environments | Separate cloud accounts; no real data in testing |
| Psychological acceptability | Controls that get bypassed | A convenient password manager instead of expiry every 30 days |
| No obscurity | Security that evaporates once the design is published | No home-grown cryptography; no "secret" paths as the only defence |
| Zero Trust | Lateral movement after the first compromise | Being on the VPN does not grant access to the database |
| Assume breach | Blindness to the incident under way | Immutable backups in another account; measurement of the time to detection |
13.2 How the tension is resolved
All these principles cost money, time or convenience. The professional way to resolve the tension has four steps:
- Classify the asset. Not everything deserves the same treatment. The medical notes in the attachments bucket and the public catalogue of services do not require the same layers. Applying the maximum to everything exhausts the budget and the team's patience, and ends in a uniformly mediocre level.
- Apply the principle proportionally. Rigorous least privilege in production; something more flexible in a development environment with no real data (and it is that condition which makes it acceptable).
- Measure the friction, do not assume it. If MFA adds 6 seconds twice a day, that is bearable; if it forces Rubén to authenticate 40 times in a morning, it will become a problem and he will find a way around it.
- Document what you accept and who accepts it. If Nimbus decides not to separate duties in systems administration because there is only one person, that gets written down, the compensating control is explained and the board signs it off. An accepted, documented risk is management; a risk accepted in silence is negligence.
Note on legal implications: several of these principles — security by design and by default, data minimisation — appear as express obligations in European data protection law. Their concrete application to your case must be validated with your compliance lead or with a legal professional; what is offered here is technical training, not legal advice.
Common Mistakes and Tips
Common mistakes:
- Granting permissions "temporarily" with no end date. The longest-lasting temporary access in the world is the one granted on a Friday to get out of a jam. Every exceptional grant must carry automatic expiry.
- Confusing defence in depth with redundancy. Layers of the same type share the same blind spots.
- Applying fail-open "so that nothing breaks". It turns an outage into a privilege escalation. If you must open up on failure, let that be a conscious, documented decision, not the
exceptthat happened to be left there. - Believing the interface protects you. Hiding a button does not protect the endpoint. Security is applied on the server, always.
- Taking simplicity as an excuse to do nothing. "Economy of mechanism" does not mean "let's not add authorisation"; it means the authorisation you do add must be comprehensible.
- Rolling out Zero Trust as a product purchase. No supplier sells you Zero Trust; they sell you a piece of it. You design the architecture yourself.
- Forgetting psychological acceptability in policies. A policy the team cannot comply with generates widespread non-compliance, and with it the loss of credibility of every other policy.
Tips:
- Faced with any decision, walk mentally through three principles: is this the least privilege possible? what happens if this fails? is there more than one layer? That covers most cases.
- Write permissions as code (SQL, policies, configuration files) and keep them in the repository. What is reviewed in a PR gets discussed; what is done by hand in a console does not.
- Start by revoking, not by granting. The default posture must be closed.
- When you have to violate a principle for practical reasons, write it down along with its compensating control. An unmet principle that is documented is manageable; one unmet in silence is not.
Exercises
Exercise 1 — Diagnosing which principles are being violated
Marta finds this situation while reviewing the Nimbus infrastructure. Identify all the principles being violated in each point and propose the fix:
- The API connects to PostgreSQL with the
postgresuser (superuser), and the same credential is used by the nightly jobs and by Lucía's manual scripts. - The internal admin panel is at
https://admin.nimbusreservas.example, with no network restriction and no MFA, but "nobody knows that URL". - The authorisation middleware has an
except Exception: return Trueblock "to avoid outages". - The password policy forces a change every 30 days with uppercase, lowercase, numbers and symbols; 60% of the team uses numbered variants of the same password.
Exercise 2 — Writing least-privilege permissions
Nimbus is going to add a new service, nimbus-reports, which generates an aggregate occupancy report per customer every night. The service:
- Reads from the
bookingsandservicestables. - Writes the result into the
occupancy_reportstable. - Uploads a PDF to the
informes/prefix of thenimbus-adjuntos-prodbucket. - Must not access
end_customersorpayroll, and must not delete anything.
Write (a) the PostgreSQL GRANT/REVOKE statements and (b) the skeleton of the bucket access policy. Justify any decision that is not obvious.
Exercise 3 — Designing defence-in-depth layers
Nimbus is going to let its customers download a CSV file containing all their bookings for the last year. It is a mass export of personal data. Design at least five independent layers of defence around this feature, stating for each layer which principle underpins it and which concrete attack or mistake it mitigates.
Solutions
Solution 1
1. Shared superuser:
- Violates least privilege: the API can drop tables, create users and read everything, when all it needs is to operate on four tables.
- Violates least common mechanism: a single credential for three different identities (API, batch, human).
- Violates separation of duties: if Lucía uses the same credential as the application, her actions are indistinguishable from the API's in the records — traceability and non-repudiation are lost.
- Fix: one role per identity (
nimbus_api_role,nimbus_batch_role, a named account for Lucía), withGRANTs narrowed as in section 2.1; no service running as superuser; administrative access on request with an expiry.
2. Panel with no real protection:
- Violates not relying on obscurity: the URL is discovered through public certificates, DNS and scanning. It is the first thing a bot does.
- Violates defence in depth: there is a single barrier, and it is fictitious.
- Violates secure by default: an admin panel should be born restricted.
- Fix: mandatory MFA, restriction to network ranges or access only through a verified identity, attempt rate limiting, logging of all accesses and an alert on out-of-hours access.
3. except Exception: return True:
- Violates fail-secure seriously: it turns any error — including one an attacker can provoke — into authorisation granted.
- Violates complete mediation: the check ceases to exist exactly when it is most needed.
- Fix: catch specific exceptions, deny access, log the error at
ERRORlevel and raise an alert. If for a business reason it were necessary to degrade open, it must be limited to non-sensitive read-only operations and be documented and approved in writing.
4. Password policy:
- Violates psychological acceptability: the burden is such that the team generates predictable patterns, weaker than a stable long password.
- Indirectly violates economy of mechanism: the complexity of the rule does not deliver proportional security.
- Fix: long passwords (passphrases), with no forced expiry unless there is an indication of compromise, checking against lists of leaked passwords, a corporate password manager and MFA. Mandatory periodic expiry is advised against by current guidance precisely because of this effect.
Solution 2
(a) PostgreSQL:
CREATE ROLE nimbus_reports_role NOLOGIN;
-- See the schema, without being able to create objects in it
REVOKE ALL ON SCHEMA public FROM PUBLIC;
GRANT USAGE ON SCHEMA public TO nimbus_reports_role;
-- Strict read access to what it needs to aggregate
GRANT SELECT ON bookings TO nimbus_reports_role;
GRANT SELECT ON services TO nimbus_reports_role;
-- Write only into its own results table; no UPDATE and no DELETE:
-- each run appends a new report, it does not rewrite the previous ones (historical integrity)
GRANT SELECT, INSERT ON occupancy_reports TO nimbus_reports_role;
GRANT USAGE ON SEQUENCE occupancy_reports_id_seq TO nimbus_reports_role;
-- Explicit prohibitions, even though they had never been granted: they document the intent
REVOKE ALL ON end_customers FROM nimbus_reports_role;
REVOKE ALL ON payroll FROM nimbus_reports_role;
REVOKE ALL ON access_audit FROM nimbus_reports_role;
CREATE USER nimbus_reports WITH PASSWORD :'generated_password';
GRANT nimbus_reports_role TO nimbus_reports;Non-obvious decisions, justified:
- No
UPDATEonoccupancy_reports: a generated report is not modified. If it needs correcting, another one is generated. This preserves the history and makes tampering easier to spot. - Explicit
REVOKEs: they protect against a futureGRANT ... ON ALL TABLES IN SCHEMA publicdone in a hurry, which would otherwise give access to personal data to a service that should only aggregate. - Better still, security by design: if the report is an aggregate, ideally it should not even read
bookingsdirectly, but a view exposing only the necessary columns (tenant_id,date_time,service_id) with no identifying data. Less privilege and less leak surface.
(b) Bucket policy:
Version: "2012-10-17"
Statement:
- Sid: UploadReportsOnly
Effect: Allow
Action: "s3:PutObject"
Resource: "arn:aws:s3:::nimbus-adjuntos-prod/informes/*" # only its own prefix
Condition:
StringEquals:
"s3:x-amz-server-side-encryption": "aws:kms"
- Sid: NoAttachmentReadsNoDeletions
Effect: Deny
Action:
- "s3:GetObject" # it does not need to read: it only writes
- "s3:DeleteObject"
- "s3:PutBucketPolicy"
- "s3:PutBucketAcl"
Resource: "arn:aws:s3:::nimbus-adjuntos-prod/*"Note: the Deny on s3:GetObject is deliberate and perhaps the most interesting point of the exercise. A service that only uploads files does not need to read the rest of the bucket, where the patients' scanned medical reports live. If the service is compromised, the attacker does not obtain that read access.
Solution 3 — Layers for the mass CSV export:
| # | Layer | Principle | What it mitigates |
|---|---|---|---|
| 1 | A specific data:export permission, not included in the basic role; only the customer's administrator holds it |
Least privilege | Any receptionist downloading the business's entire database |
| 2 | Re-verification of identity (MFA) at the moment of exporting, even if the session is already open | Complete mediation, need to know | Use of a stolen session or of an unattended machine |
| 3 | A mandatory tenant_id filter applied in the database through RLS, not in the query |
Security by design | An oversight in the code exporting other customers' data |
| 4 | A range limit (maximum 12 months), a size limit and a frequency limit (one export every 24 h per customer) | Fail-secure, availability | Repeated mass exfiltration; resource exhaustion |
| 5 | Asynchronous generation and delivery through a signed URL expiring in 15 minutes, single use, never attached to an e-mail | Least privilege, defence in depth | Accidental forwarding of the link; permanent, indexable links |
| 6 | An audit record with actor, tenant, rows exported, IP and time, in storage with no delete permission | Traceability, non-repudiation, separation of duties | Later denial; inability to investigate an incident |
| 7 | An automatic alert to Marta and an e-mail to the customer's administrator on every export | Assume breach | Silent exfiltration through a compromised account |
| 8 | Minimal columns in the CSV (no internal notes, no payment identifiers) and encryption of the file at rest | Need to know, least privilege | A leaked CSV exposing more than the bare minimum |
The layers are independent: a failure of the permission check (1) does not cancel the database isolation (3), and even if the file leaks, its content is minimised (8) and the fact is recorded and alerted on (6, 7). An additional note: an export of personal data has compliance implications — lawful basis, information to the data subject, recording of the activity — that must be validated with the data protection officer.
Conclusion
You have worked through the fifteen principles that underpin every technical decision in the rest of the course. Least privilege and need to know limit the damage of any failure, and you have seen them made concrete in the GRANTs of the nimbus_api account — no DELETE, no access to payroll, INSERT but not UPDATE on the audit table — and in a bucket policy that stops the API itself deleting objects or rewriting the rules. Defence in depth stacks independent layers around the medical attachments, including the detection and recovery layers that so many architectures forget. Secure by default and security by design move protection from "let nobody forget" to "forgetting is impossible", as row level security in PostgreSQL does. Fail-secure has taught you that a badly placed except turns an outage into a privilege escalation.
You have also seen the principles that are forgotten most often and cost the most: separation of duties with enforce_admins: true and its compensating controls when an SME has only one administrator; complete mediation, which checks again on every download; economy of mechanism, which requires you to be able to draw your authorisation model on a whiteboard; psychological acceptability, which explains why the strictest password policy produces the weakest passwords; and the rejection of security through obscurity, with the distinction between a rotatable secret and a hidden design. And you have closed with the two ideas that define modern security: Zero Trust — location on the network grants no privileges — and assume breach, which changes the question from "how do I stop them?" to "how much damage can they do and how long before I find out?".
You now know what to protect, what from and on what basis. What is missing is the most concrete part: knowing exactly what Nimbus has. In the next lesson, Assets, Attack Surface and Threat Actors (01-04), we will build the company's asset inventory with owner, criticality and information classification; we will measure its attack surface in all its dimensions; we will meet the actors who might attack it and why an SME is affected above all by automated attacks; and you will take your first steps with STRIDE over a real Nimbus data flow diagram.
Fundamentals of Information Security Course
Module 1: Introduction to Information Security
- Basic Concepts of Information Security
- Types of Threats and Vulnerabilities
- Principles of Information Security
- Assets, Attack Surface and Threat Actors
Module 2: Cybersecurity
- Definition and Scope of Cybersecurity
- Types of Cyber Attacks
- Social Engineering and Phishing
- Protection Measures in Cybersecurity
- Identity, Authentication and Access Control
- Cybersecurity Incident Case Studies
Module 3: Cryptography
- Introduction to Cryptography
- Symmetric Cryptography
- Asymmetric Cryptography
- Hash Functions, HMAC and Password Storage
- Cryptographic Protocols
- Key Management, Certificates and PKI
- Applications of Cryptography
Module 4: Risk Management and Protection Measures
- Risk Assessment
- Security Policies
- Security Controls
- Third-Party and Supply Chain Risk
- Incident Response Plan
- Disaster Recovery and Business Continuity
Module 5: Security Tools and Techniques
- Vulnerability Analysis Tools
- Monitoring and Detection Techniques
- Penetration Testing
- Network Security
- Application Security
- System Hardening and Endpoint Security
- Cloud and Container Security
Module 6: Best Practices and Regulations
- Best Practices in Information Security
- Security Regulations and Standards
- Personal Data Protection and GDPR in Practice
- Compliance and Auditing
- Training and Awareness
- Ethics, Legal Aspects and Responsible Disclosure
