Security gets treated as a feature set: authentication, authorization, input validation, encryption. Implement those features and the system is secure. This framing is wrong and produces systems that are secure in obvious ways while vulnerable in non-obvious ones.
Security is an emergent property of architectural decisions: where trust boundaries are placed, how data flows between components, what permissions components have, how failures are handled. These decisions are made during design, not during a security review at the end.
Threat Modeling: Starting With Adversarial Thinking
Security work should start with threat modeling — systematically thinking about what an adversary could do and what the consequences would be.
The STRIDE framework provides a useful taxonomy:
- Spoofing: impersonating a user or system
- Tampering: modifying data or code
- Repudiation: denying an action occurred
- Information Disclosure: exposing data to unauthorized parties
- Denial of Service: preventing legitimate use
- Elevation of Privilege: gaining permissions beyond what’s authorized
For each significant component and data flow, ask: what could an adversary do? What’s the impact? What’s the likelihood?
Simple threat modeling exercise for an API:
Component: Order API endpoint (POST /orders)
Data flows:
- User credentials → Auth service
- Order data → Orders service
- Payment details → Payment service
Threats:
T1: SPOOFING — attacker impersonates authenticated user
Impact: orders placed under victim's account
Mitigation: short-lived JWT tokens, MFA for sensitive operations
T2: TAMPERING — attacker modifies order data in transit
Impact: wrong items/quantities ordered
Mitigation: HTTPS, request signing
T3: INFORMATION DISCLOSURE — order data leaks
Impact: customer data exposure, GDPR violation
Mitigation: authorization checks on every read, minimal data in logs
T4: ELEVATION OF PRIVILEGE — user accesses other users' orders
Impact: data exposure, potential for fraud
Mitigation: resource-level authorization (verify order belongs to user)
This doesn’t need to be exhaustive. Even a 30-minute threat modeling exercise before architecture decisions surfaces the risks that deserve deliberate design.
Trust Boundaries
A trust boundary is a line where the security context changes. Every component and data flow crosses trust boundaries.
Explicitly designing trust boundaries reveals implicit trust relationships that shouldn’t exist:
External (no trust)
↓ [public internet]
Load Balancer / WAF (minimal trust — rate limiting, basic filtering)
↓ [internal network]
API Gateway (authenticated trust — JWT verification)
↓ [service mesh with mTLS]
Services (service identity trust — knows who's calling)
↓ [VPC internal]
Database (no external access — only from specific service IPs)
↓
Storage (encrypted at rest — no direct access from applications)
Implicit trust is dangerous. “Services in our VPC can trust each other” is an implicit trust model that means a compromise of any service gives an attacker access to all services. Service mesh mTLS, service-to-service JWT tokens, or policy-based authorization (OPA) at the service level reduces the blast radius of any single service compromise.
Least Privilege in Practice
The principle of least privilege: components should have the minimum permissions required to do their job.
This applies at every layer:
IAM roles (AWS example):
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:PutObject"
],
"Resource": "arn:aws:s3:::orders-bucket/orders/*"
}
]
}
Not s3:*. Not arn:aws:s3:::*. Specifically what this service needs.
Database permissions:
-- Create a role with only what the application needs
CREATE ROLE orders_app_role;
GRANT SELECT, INSERT, UPDATE ON orders.orders TO orders_app_role;
GRANT SELECT ON orders.products TO orders_app_role;
-- NOT: GRANT ALL PRIVILEGES
-- The application user gets this role
CREATE USER orders_app WITH PASSWORD '...';
GRANT orders_app_role TO orders_app;
An application user that can only SELECT, INSERT, UPDATE on specific tables cannot DROP tables, cannot access other schemas, and cannot be used for SQL injection attacks that go beyond reading/writing orders data.
Service-to-service permissions: each service’s token should only authorize it to call the specific endpoints it needs. An inventory service token shouldn’t authorize calls to the payment service.
Secrets Management
Hardcoded credentials and credentials in environment variables are common and dangerous. Environment variables are logged, visible in container orchestration UIs, and accessible to every process in the container.
Use a secrets manager:
- AWS Secrets Manager / Parameter Store
- HashiCorp Vault
- Kubernetes Secrets (with encryption at rest enabled)
// Spring Boot with AWS Secrets Manager
@Configuration
public class DatabaseConfig {
@Value("${spring.datasource.password}") // Injected from Secrets Manager
private String dbPassword;
}
// application.yml
spring:
config:
import: aws-secretsmanager:/production/postgres
datasource:
password: ${DB_PASSWORD}
With Spring Cloud AWS, secrets from AWS Secrets Manager are injected as Spring properties. The credentials are never on disk, never in environment variables, and rotated automatically.
Secrets rotation: credentials should rotate regularly. Systems that can’t survive credential rotation are fragile. Design for rotation from the start.
Dependency Security
The npm/Maven/Gradle dependency graph is an attack surface. Supply chain attacks (malicious packages) and known vulnerabilities in dependencies are real vectors.
Dependency scanning in CI/CD:
# GitLab CI
dependency-scan:
image: owasp/dependency-check:latest
script:
- /usr/share/dependency-check/bin/dependency-check.sh
--project "myapp"
--scan "."
--format "JSON"
--failOnCVSS 7 # Fail on high severity
artifacts:
when: always
paths: [dependency-check-report.json]
Tools: OWASP Dependency-Check, Snyk, GitHub Dependabot, Trivy.
Pin dependency versions in production. latest is convenient; it’s also “whatever the registry says today,” which may be compromised. Pin to specific versions and update deliberately.
Minimize the dependency surface. Every dependency is a potential vulnerability. Libraries that are small, maintained, and well-audited are preferable to large, complex ones.
Data Classification
Not all data has the same sensitivity. A security architecture that treats everything the same is either too restrictive (slows down legitimate work) or too permissive (exposes sensitive data).
Define data classification levels:
- Public: can be shown to anyone
- Internal: for authenticated users of the system
- Confidential: specific role/permission required
- Sensitive/PII: personal data, payment data, credentials — additional controls
Data flows should be designed around classification: sensitive data doesn’t flow through systems designed for public data, logs don’t capture sensitive values, caches don’t store credentials.
// Log sanitization — don't log sensitive fields
public record OrderRequest(
String productId,
int quantity,
@Sensitive String cardNumber, // Custom annotation
@Sensitive String cvv
) {
@Override
public String toString() {
// Custom toString that masks sensitive fields
return "OrderRequest{productId=%s, quantity=%d, cardNumber=****}"
.formatted(productId, quantity);
}
}
Network Security
Default-deny network policies: by default, no service can talk to any other service. Explicitly allow only required communication.
# Kubernetes NetworkPolicy
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: orders-policy
spec:
podSelector:
matchLabels:
app: orders-service
ingress:
- from:
- podSelector:
matchLabels:
app: api-gateway # Only allow from API gateway
egress:
- to:
- podSelector:
matchLabels:
app: postgres # Only allow to postgres
ports:
- port: 5432
An attacker who compromises one service is contained to that service’s allowed network connections.
Security as Design, Not Review
Security reviews at the end of a project are useful for catching obvious mistakes. They can’t fix architectural decisions that have been committed to.
Security architecture decisions happen at the start:
- Where are the trust boundaries?
- What data is sensitive and how does it flow?
- What permissions does each component need?
- What happens when a component is compromised?
These questions are architectural questions. Answer them during design, not during a security review after the architecture is finalized.