The Architecture at a Glance
Before we talk about securing anything, let's agree on what we're securing. A three-tier architecture separates your application into three distinct layers, each with a job and a blast radius.
Tier 1 — Edge & Web
Tier 2 — Application
Tier 3 — Data
Cross-cutting
Three rules before we start: every tier only talks to the tier directly adjacent to it, no tier gets more permissions than it needs for its specific job, and everything is logged — but not everything needs to be logged at the same cost.
Why CIA Triad Maps Cleanly to Three Tiers
The three-tier architecture isn't just a performance decision. It's a security architecture, whether you designed it that way or not. Each tier has a different relationship with the CIA triad — and understanding that relationship is what separates a secure deployment from a compliant-on-paper one.
Tier 1 (Edge & Web): Availability is your primary concern. This is what users hit. DDoS protection, rate limiting, and redundancy live here. If this tier goes down, the business feels it immediately.
Tier 2 (Application): Integrity is your primary concern. Business logic lives here, and business logic is where manipulation attacks happen. Price tampering, workflow bypass, race conditions, injection attacks — all exploited at this layer.
Tier 3 (Data): Confidentiality is your primary concern. The data is what attackers want. Encryption, access controls, and audit trails belong here. A breach at this layer is the breach that ends up in the news.
None of these concerns are exclusive — you need all three at all three tiers. But knowing which is primary tells you where to spend your effort first.
STRIDE at Each Layer
STRIDE (Spoofing, Tampering, Repudiation, Information Disclosure, Denial of Service, Elevation of Privilege) is a threat modeling framework, not a checklist. Running it against a three-tier architecture gives you a structured way to think about what can go wrong at each boundary rather than guessing.
Tier 1 — Edge & Web Layer
| Threat | How it manifests | Control |
|---|---|---|
| Spoofing | Attacker forges a client IP or impersonates a trusted CDN | Validate X-Forwarded-For headers server-side; pin trusted CDN IP ranges in security groups |
| Tampering | HTTP request modification before it reaches the ALB | WAF rules; signed cookies; TLS everywhere, no unencrypted path |
| Repudiation | User denies making a request | Access logs with IP, user agent, timestamp, request ID; tie to session |
| Information Disclosure | Server version headers, error stack traces in responses | Strip Server and X-Powered-By headers; custom error pages that reveal nothing |
| Denial of Service | Volumetric attack, slowloris, large payloads | WAF rate limiting, DDoS scrubbing (Shield Advanced), payload size caps, connection limits |
| Elevation of Privilege | SSRF through the web server reaching internal metadata | Enforce IMDSv2 on all EC2 instances (defense-in-depth, not a full SSRF fix); URL allowlisting, redirect validation, and blocking outbound access to private/link-local destinations |
Tier 2 — Application Layer
| Threat | How it manifests | Control |
|---|---|---|
| Spoofing | JWT forgery, session token theft, credential stuffing | Short-lived JWTs, signed with an algorithm matched to your trust boundary (RS256/ES256 across services, HS256 only within a tightly controlled boundary); HttpOnly + Secure + SameSite=Strict cookies; JTI revocation list |
| Tampering | SQL injection, mass assignment, business logic manipulation | Parameterised queries everywhere; DTO allowlists; atomic DB operations for financial state |
| Repudiation | "I never made that transaction" | Structured audit log capturing who, what, when, from where, with what auth token |
| Information Disclosure | Verbose error messages leaking table names, stack traces | Production error handler returns opaque IDs; detailed errors only to log infrastructure |
| Denial of Service | Resource exhaustion through expensive queries, no pagination | Query complexity limits; mandatory pagination; async processing for heavy operations |
| Elevation of Privilege | IDOR — accessing another user's resources | Scope every DB query to the authenticated user; Row-Level Security as a second enforcement layer |
Tier 3 — Data Layer
| Threat | How it manifests | Control |
|---|---|---|
| Spoofing | App server impersonating a more privileged DB user | Separate DB credentials per application service; no shared superuser account |
| Tampering | Direct DB modification bypassing application controls | DB audit log for every write; RLS policies enforced below the application layer |
| Repudiation | DBA denies running a query that modified data | DB audit log with query text, timestamp, and originating user; append-only audit table for sensitive operations |
| Information Disclosure | DB snapshot made public; unencrypted backup to S3 | Encryption at rest (KMS); no public snapshots; S3 Block Public Access for backup bucket |
| Denial of Service | Query flooding exhausting connection pool | Connection pooling (PgBouncer/RDS Proxy); query timeout limits; read replica for reporting |
| Elevation of Privilege | App user running DDL or accessing schemas it shouldn't | Principle of least privilege: the application's DB user can only SELECT/INSERT/UPDATE on its own schema, nothing else |
Tier 1: Securing the Edge & Web Layer
TLS — Get This Completely Right
TLS is table stakes, but table stakes that people still misconfigure. Terminate TLS at the ALB with a certificate from ACM (automatic renewal, no manual certificate management). Between the ALB and your web servers, you have a choice: plain HTTP on an internal private network, or TLS again. The answer depends on your threat model. If you're handling regulated data or have compliance requirements (PCI-DSS, HIPAA), terminate TLS at every hop. If you're on a well-segmented VPC with no lateral movement risk, HTTP internally is often an acceptable trade-off that reduces operational complexity.
What is non-negotiable:
- TLS 1.2 minimum, TLS 1.3 preferred
- No self-signed certificates in any environment (break trust validation habits)
- HSTS header with a minimum of one year:
Strict-Transport-Security: max-age=31536000; includeSubDomains - OCSP stapling enabled on the web server
Security Headers That Actually Matter
Every response from your web servers should include:
Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-{random}'; object-src 'none'
X-Frame-Options: DENY
X-Content-Type-Options: nosniff
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: camera=(), microphone=(), geolocation=()
Remove Server, X-Powered-By, and X-AspNet-Version. These headers tell an attacker exactly what software version to look up CVEs for.
WAF Configuration
A WAF without tuning is a false sense of security. The AWS Managed Rules for Common Vulnerabilities are a decent starting point, but they'll generate false positives against legitimate traffic if you deploy them in block mode on day one. Start in count mode, review the logs for two weeks, suppress the false positives, then switch to block mode.
Rules that should always be in block mode regardless of false positive risk:
- SQL injection patterns
- Log4j detection rules (yes, still)
- Scanner/automated tool signatures
- Requests without a User-Agent header (legitimate browsers always send one)
Rate limiting rules:
- Per IP: 2,000 requests per 5-minute window for most APIs
- Per IP for auth endpoints: 20 requests per 5 minutes, no exceptions
Web Server Role: Proxy, Not Logic
The web server in this tier (Nginx, Apache, Caddy) has one job: handle TLS termination, serve static assets, and proxy requests upstream to the application tier. No business logic runs here. Authentication, authorisation, database calls, and anything that touches your data — that all lives in Tier 2. Keeping the web server dumb is intentional. A compromised web server should be a dead end, not a foothold into your application.
Network Segmentation
Only the ALB needs to be internet-reachable in this tier — the web server itself doesn't. Put the ALB in a public subnet with a route to the internet gateway, and put the web server in a private subnet with no direct internet route, same as the application tier. The ALB is what terminates the internet-facing connection and forwards to the web server over the private network; the web server never needs a public IP or an IGW route of its own. Any outbound internet request the web server makes (to call a third-party API, for example) goes through a NAT Gateway, exactly like the application tier.
Security group rules for the web server:
Inbound:
Port 443 from ALB SG only (internet HTTPS reaches the ALB, not the web server directly)
Port 80 from ALB SG only (redirect to 443 only)
Outbound:
Port 8080 to Application Tier SG (app traffic only)
Port 443 to 0.0.0.0/0 (HTTPS outbound for third-party calls, via NAT)
The ALB security group should be the only thing that can reach port 443 on your web servers — not any IP, not any other SG, the specific ALB SG. Reference security groups, not IP ranges. Putting the web server in a private subnet makes this the enforced reality rather than just an SG policy: there's no route from the internet to the web server's ENI at all, ALB or not.
Tier 2: Securing the Application Layer
Authentication and Session Management
Your application servers are where authentication logic lives, and authentication is where most meaningful compromises begin. A few hard rules:
JWT, if you use it: For distributed systems, asymmetric signing such as RS256 or ES256 is generally preferable because services can verify tokens without possessing the signing key. HS256 can be appropriate when the issuer and verifiers share a tightly controlled trust boundary. Whichever you choose:
- Reject
alg=noneoutright — never accept an unsigned token. - Don't allow algorithm confusion: hardcode the expected algorithm in your verification code, never read it from the token header.
- Validate the issuer (
iss). - Validate the audience (
aud). - Validate expiry (
exp), with a short lifetime (15 minutes to 1 hour) and revocable refresh tokens. - Rotate signing keys on a schedule, and support key rollover (e.g. via a JWKS endpoint with multiple active keys) so rotation doesn't invalidate every in-flight token at once.
Implement a JTI (JWT ID) blocklist in Redis for immediate session invalidation on logout.
Session cookies: HttpOnly, Secure, SameSite=Strict. The SameSite attribute alone eliminates the vast majority of CSRF risk without a separate token mechanism. Add a CSRF token for any form that changes state, as defense in depth.
Secrets: Nothing hardcoded. Nothing in environment files committed to git. Everything in a secrets manager (AWS Secrets Manager or HashiCorp Vault) with automatic rotation and audit on every access. The application fetches secrets at startup with a short-lived IAM role — not long-lived access keys, roles.
Input Validation and Injection Prevention
Every input that enters your application is potentially hostile. Validate at the boundary:
# The difference between vulnerable and safe is one line
# Vulnerable:
cursor.execute(f"SELECT * FROM orders WHERE user_id = {user_id}")
# Safe — the value is bound after the query structure is compiled
cursor.execute("SELECT * FROM orders WHERE user_id = %s", (user_id,))
For mass assignment (a frequently overlooked but critical vulnerability): never bind a request body directly to a database model. Define an explicit list of fields the API is allowed to set. If role or isAdmin can't be in that list, they can't be set by a client no matter what the request contains.
Service-to-Service Authentication
Your application tier calls your data tier. How do those calls authenticate? If the answer is "a shared username and password in a config file," you have work to do.
Each application service should have its own database credential with the minimum permissions it needs. A service that only reads data gets a read-only user. A service that writes orders gets INSERT/UPDATE on the orders table, nothing else. If a service is compromised, its credential's blast radius is bounded by what that credential can actually do.
For service-to-service HTTP calls within the application tier (microservices calling microservices), mutual TLS with SPIFFE/SPIRE identities is the right answer at scale. At smaller scale, a signed JWT passed in the Authorization header achieves the same property: the receiving service can verify the caller's identity without needing a shared secret.
Tier 3: Securing the Data Layer
Network Isolation First
Your database should be completely invisible from the internet and from the edge & web tier. The security group for the database accepts connections on the DB port from one source: the application tier security group. That's the entire rule set. There is no internet access, no NAT, no management ports open from anywhere other than a bastion or AWS Systems Manager Session Manager.
Encryption Everywhere
At rest: Enable encryption on your RDS instances (AES-256, KMS-managed keys). This is a checkbox on creation — there's no reason not to. EBS volumes backing the instances are encrypted. Snapshots inherit the encryption of the source. Unencrypted snapshots that are accidentally made public are one of the most common data breach vectors in cloud environments.
In transit: Require TLS for all connections to the database. In RDS this is enforced with a parameter group setting: rds.force_ssl = 1. Connections that don't present a certificate are rejected. There is no "well, it's on an internal network, it's probably fine" — internal networks get compromised, and then your database traffic is readable to whoever is on that network.
Key management: Use KMS with a customer-managed key (CMK) for sensitive data. This lets you revoke access to data by deleting the key — useful for GDPR right-to-erasure requests and for crypto-shredding when offboarding a tenant.
Row-Level Security
PostgreSQL Row-Level Security (RLS) is one of the most underused security controls in web applications. It enforces data isolation at the database engine level, below the application layer:
-- Enable RLS on the sensitive table
ALTER TABLE customer_data ENABLE ROW LEVEL SECURITY;
-- Policy: the application can only see rows belonging to
-- the tenant set in the session context
CREATE POLICY tenant_isolation ON customer_data
USING (tenant_id = current_setting('app.tenant_id')::uuid);
-- The application sets the context before each query
SET app.tenant_id = '550e8400-e29b-41d4-a716-446655440000';
Now, even if a bug in the application drops a WHERE clause, the database adds it back automatically. Cross-tenant data leakage is architecturally prevented, not just application-level prevented.
Logging Strategy: What to Enable, What to Skip
Logging everything is expensive and often counterproductive — the alerts you care about drown in noise. Logging nothing means you're flying blind during an incident. The right answer is tiered logging with cost controls.
Tier 1 — Edge & Web
Enable (always):
- ALB access logs → S3 (cost: low, $0.01 per GB stored). Every request, every response code, every backend target. This is your forensic baseline.
- WAF logs → S3 or Kinesis Firehose (cost: ~$0.60 per million requests). You need to know what the WAF is blocking to tune rules and to prove you were under attack.
- CloudFront or CDN access logs if applicable.
Enable selectively:
- VPC Flow Logs at the REJECT level by default (accepted traffic is noise at scale). Set a 7-day retention in CloudWatch Logs before archiving to S3. REJECT-only Flow Logs can be a cost-conscious baseline, but security-sensitive workloads may require ACCEPT traffic visibility as well, particularly for detecting unexpected east-west or egress connections. One caveat: this doesn't cover instance metadata access — AWS excludes traffic to
169.254.169.254from Flow Logs entirely, at any level, because IMDS is served locally by the hypervisor and never traverses the ENI the way Flow Logs capture. For that specific signal you need GuardDuty (UnauthorizedAccess:EC2/MetadataDNSRebind,InstanceCredentialExfiltration), CloudTrail anomaly detection on unexpected API calls from the instance role, or host-level monitoring — not Flow Logs.
Skip unless regulated:
- Full packet capture — prohibitively expensive and rarely necessary for most applications.
Tier 2 — Application
Enable (always):
- Structured application logs in JSON format. Not
console.log("User logged in"). Every log line should have:timestamp,request_id,user_id,action,outcome,source_ip. A request ID that propagates through every service call is what makes incident investigation possible. - Authentication events: every login attempt, every token refresh, every logout, every failed auth. These are cheap to log and invaluable for detecting credential stuffing.
Enable selectively:
- Full request/response logging for payment or financial endpoints. Not for general API traffic — the volume is too high and the cost compounds.
- Slow query logs from the application ORM, with a threshold of 500ms.
Skip:
- Logging request bodies that contain passwords or card numbers. Obviously. But worth stating because it happens.
Tier 3 — Data
Enable (always):
- RDS Enhanced Monitoring (1-second granularity — costs about $14/month per instance, worth it).
- RDS audit log (pgaudit for PostgreSQL): log all DDL statements and authentication events always. Log DML (SELECT, INSERT, UPDATE, DELETE) on tables containing PII or financial data, not on all tables.
- CloudTrail management events (API calls, console logins, IAM changes) — free for the first copy per region. Always on, no exceptions. This is your forensic backbone and costs nothing.
CloudTrail data events (object-level S3 operations, Lambda invocations) — $0.10 per 100,000 events. Enable selectively: only on S3 buckets containing sensitive data (RDS snapshots, backups, PII exports) and Lambda functions handling auth or payments. Not on every bucket and function — the cost compounds fast at scale.
Skip unless required:
- Logging every single SELECT on high-traffic tables. The cost scales linearly with queries and will surprise you at the end of the month.
Cross-Cutting: Log Centralization and Retention
Ship all logs to a centralized destination (CloudWatch Logs for querying, S3 for long-term storage). Crucially, your production account should not be able to delete its own logs. Set up a separate security account that receives logs via cross-account CloudTrail and S3 replication, with object lock enabled. An attacker who compromises your production environment can disable CloudTrail in that account — they cannot touch the copy in a separate account they've never been given access to.
Retention by cost:
- Hot (CloudWatch Logs, queryable): 30 days
- Warm (S3 Standard): 90 days
- Cold (S3 Glacier): 1–7 years depending on regulatory requirements
What Happens During an Attack
Understanding what an active attack looks like in a three-tier architecture is how you build the defenses that actually matter. Most attackers don't kick in the front door — they probe systematically, find the gap, and move laterally.
A Realistic Attack Scenario
An attacker identifies your application through subdomain enumeration. They find an endpoint that was deployed for a one-time integration and never decommissioned — a forgotten webhook receiver running on the application tier without authentication. Through this endpoint they can make the application server make outbound HTTP requests (SSRF).
Phase 1 — Scanning (what generates the 404 spike): The first requests come from a residential proxy network, rotating IPs, with realistic user agents. Your WAF rate limiting doesn't trigger because each IP only makes a handful of requests. The attacker is enumerating paths — probing for endpoints that might exist. This phase produces your first detectable signal: a spike in 404 responses to paths that don't exist in your application. Monitor this. A single IP hitting 50+ non-existent paths in a short window is a scanner, not a user.
Phase 2 — Exploitation (the forgotten endpoint is found): Eventually the scanner finds the forgotten webhook receiver. Unlike the paths that returned 404, this one returns 200 or 403 — it exists. That's a different signal entirely, and one most teams miss because they're only watching for 404 spikes. The attacker now has a live, unauthenticated endpoint they can interact with.
Phase 3 — SSRF to metadata: Through the SSRF endpoint, the attacker hits http://169.254.169.254/latest/meta-data/iam/security-credentials/. If IMDSv2 is not enforced, they get the application server's IAM role credentials. This is the pivot point — everything changes here.
Phase 4 — Lateral movement via IAM: With the instance role credentials, they call AWS APIs from their own machine. Depending on what the role can do, this is either a dead end (if the role is least-privilege) or a path to the rest of your infrastructure.
What stops this at each phase:
- Tier 1: Attack surface monitoring that would have found the forgotten endpoint
- Tier 2: IMDSv2 enforcement — reduces but does not eliminate the risk (see note below)
- Tier 3: IAM role scoped to only the permissions the application actually needs
Detection Signals to Watch
The signals you should have alerts on that would catch this pattern:
1. First-seen source IP accessing sensitive endpoints
Signal: any auth/API endpoint accessed from an IP not seen in the prior 7 days
Alert: Medium (batch hourly, not real-time)
2. Application making outbound HTTP requests to internal
(RFC-1918) IPs it shouldn't be reaching
Signal: VPC Flow Logs ACCEPT records showing app server
connecting to an internal IP outside its normal dependency list
Alert: Critical (real-time)
3. Application making outbound requests to the metadata endpoint
Signal: NOT VPC Flow Logs — AWS excludes 169.254.169.254 traffic
from Flow Logs entirely. Use GuardDuty
(InstanceCredentialExfiltration, MetadataDNSRebind) or
CloudTrail anomaly detection on the instance role's API calls
Alert: Critical (real-time)
3. IAM credentials used from a non-EC2 IP
Signal: CloudTrail showing API calls from instance profile credentials
but originating from an IP not belonging to your EC2 instances
Alert: Critical (real-time) — this is the smoke alarm
4. Spike in 4xx responses from a single source ASN
Signal: ALB access logs grouped by AS number
Alert: Medium (5-minute window)
GuardDuty covers items 2 and 3 out of the box. Items 1 and 4 require custom SIEM queries or CloudWatch metric filters.
Patching and Hot-Swapping During an Attack Without Downtime
This is the question that separates teams who have practiced this from teams who only think they have. Patching during an active incident is not about urgency — it's about the operational discipline you built before the incident started.
The Prerequisite: Immutable Infrastructure
If your instances are snowflakes — configured by hand, with SSH logins and manual software installs — then patching during an incident means logging into production servers while an attacker may be watching. That's not a place you want to be.
The right model: your EC2 instances are baked from a golden AMI. Every software change produces a new AMI. Patching means deploying a new AMI, not running apt upgrade on a live server.
The Hot-Swap Pattern
Here's how you push a patched version to any tier without dropping a single production request:
For the Application Tier (the most common patch target):
Current state:
ALB → Target Group A → [App-1, App-2, App-3] (running vulnerable version)
Step 1: Build the patched AMI
EC2 Image Builder runs, patches the OS/application, validates, publishes AMI-v2.
Step 2: Create a new Launch Template version pointing to AMI-v2.
Step 3: Initiate an ASG Instance Refresh with these parameters:
MinHealthyPercentage: 80
InstanceWarmup: 60 seconds
Checkpoint (optional): pause after 20% to verify health
What happens:
- ASG launches App-4 from AMI-v2
- Waits for App-4 to pass health checks (60s warmup)
- ALB starts sending traffic to App-4
- ASG drains App-1 (connection draining: 30s)
- App-1 terminated after all connections close
- Repeats for App-2, App-3
Current state at no point during this process:
- Zero production requests dropped
- At minimum 80% of original capacity always serving
- Traffic never routes to an instance that hasn't passed health checks
The key detail is connection draining. When the ALB deregisters an instance, it doesn't terminate connections mid-flight. It stops sending new connections and waits up to your configured deregistration delay (default 300 seconds, set to 30-60 for most applications) for existing connections to finish. Only then does the ASG terminate the instance.
For a Zero-Day That Requires a Compensating Control Before the Patch is Ready:
Sometimes the patch takes hours to build and validate, but the attack is happening now. Compensating controls buy time without touching your instances at all:
1. WAF rule: block the specific request pattern that exploits the vulnerability
Deploy in minutes. No instance changes.
This is why WAF rules need to be deployable as code, not via console clicks.
2. Feature flag: if the vulnerable feature can be toggled, disable it at the
application config layer (SSM Parameter Store update, no deploy required).
3. Network ACL: if the attack is coming from an identifiable IP range,
block at the subnet level. This works in addition to WAF for volumetric attacks.
A compensating control is not a fix. It is a firebreak that gives your team time to build and validate the actual fix without making a hurried, error-prone change to production infrastructure during an active incident.
For the Database Tier
Database patching is the hardest because it's stateful. The procedure for RDS:
1. Enable Multi-AZ if not already (this should always be on for production)
With Multi-AZ, there's already a synchronous standby instance.
2. Initiate a maintenance window patch:
- RDS performs a failover to the standby (60-120 seconds of reconnection)
- The primary is patched
- Another failover returns the primary to the original AZ
Total downtime: the reconnection time, not the patch time
3. For major version upgrades (more disruptive):
- Create a read replica running the new version
- Validate application compatibility against the replica
- Promote the replica to primary
- Update connection strings (or use a proxy that handles this)
This is the same hot-swap pattern as the application tier — parallel deployment, cutover when ready
For attacks specifically targeting the database (SQL injection leading to data exfiltration): the immediate response is not to patch — it's to revoke the application's database credential and issue a new one. AWS Secrets Manager with rotation can do this programmatically. The application picks up the new credential automatically without a restart if you implement proper credential fetching with retry logic.
The Short List: What You Must Have Before You Go Live
A three-tier architecture is not inherently secure. It's a structure that makes security controls straightforward to implement if you know where they go. Here's the minimum bar:
Network:
- Subnets segmented by trust boundary (public for the ALB only, private for web and application servers, isolated for data), a security group per tier, no cross-tier traffic except on the expected port and direction
- No direct internet access from the web, application, or database tier — only the ALB is internet-facing
- IMDSv2 enforced on all EC2 instances
- VPC Flow Logs enabled at REJECT level by default, ACCEPT logging added for security-sensitive subnets watching for unexpected east-west/egress connections
- GuardDuty enabled for metadata-credential-theft detection (Flow Logs do not capture traffic to the instance metadata endpoint)
Identity and Access:
- No long-lived IAM user access keys; IAM roles for all workloads
- Separate database credentials per service, minimum required permissions
- MFA on all human IAM accounts; SCPs preventing root from being used for workloads
Data:
- Encryption at rest on all RDS instances and EBS volumes
- TLS enforced for all database connections
- No public RDS snapshots (ever)
- S3 Block Public Access enabled account-wide
Detection:
- CloudTrail enabled in all regions, log validation on, shipped to a separate account
- GuardDuty enabled organization-wide
- Alerts on: CloudTrail disabled, root login, IAM credentials used from unexpected IP, security group opens 0.0.0.0/0
Operational Readiness:
- ASG with health checks for every application tier service
- Connection draining configured on ALB target groups
- One practiced failover drill for the database tier before go-live
Final Thought
The three-tier architecture has been the dominant pattern in web application infrastructure for twenty years for good reason: it creates natural security boundaries that are hard to mess up accidentally once you've made the right initial decisions. The subnet isolation, the security group rules, the IAM boundaries — these are controls that protect you even when the application code has a bug, because the architecture limits what that bug can be used to reach.
Security doesn't come from the architecture diagram. It comes from the discipline of enforcing the boundaries the diagram describes. The edge & web tier talks to the application tier, the application tier talks to the database tier, and nothing else is normal — which means anything else is a signal worth investigating.
That's the actual security posture. Not a list of tools. Enforced boundaries, logged violations, practiced responses.