Security — Architect-Level Interview Guide
Target: Senior Engineer · Engineering Lead · Pre-Architect Focus: Authentication, Authorization, Inter-Service Security, Data Protection
Q: How do you implement authentication and authorization in microservices using OAuth2 and JWT?
Why interviewers ask this: Tests understanding of distributed identity, token propagation, and the difference between AuthN and AuthZ across service boundaries.
Answer
In a microservices architecture, authentication is centralized (identity provider / Auth server) while authorization is distributed (each service enforces its own rules).
Flow:
- Client authenticates with the Auth Server (Keycloak, Okta, Auth0)
- Auth Server issues a signed JWT access token
- Client sends token in
Authorization: Bearer <token>header - API Gateway validates the token signature and expiry
- Downstream services receive the token, verify the claims, and enforce role/scope rules locally
JWT structure:
Header.Payload.Signature
eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJ1c2VyMSIsInJvbGVzIjpbIkFETUlOIl0sImV4cCI6MTY5MDAwMH0.sig
Spring Boot configuration:
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.oauth2ResourceServer(oauth2 ->
oauth2.jwt(jwt -> jwt.jwtAuthenticationConverter(jwtAuthConverter()))
)
.authorizeHttpRequests(auth -> auth
.requestMatchers("/actuator/health").permitAll()
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
);
return http.build();
}
}
Key design decisions:
| Decision | Recommendation |
|---|---|
| Token format | JWT (stateless) over opaque tokens for microservices |
| Token validation | Each service validates signature locally using public key |
| Token propagation | Pass token downstream via headers; never re-issue |
| Token expiry | Short-lived access tokens (15 min) + refresh tokens |
| Key management | Rotate signing keys; use JWKS endpoint for public keys |
graph LR
Client["Client App"]
Auth["Auth Server · Keycloak"]
Gateway["API Gateway · Token Validation"]
SvcA["Service A · Role Check"]
SvcB["Service B · Scope Check"]
Client -->|1 · Login credentials| Auth
Auth -->|2 · JWT access token| Client
Client -->|3 · Bearer token| Gateway
Gateway -->|4 · Validate signature| Gateway
Gateway -->|5 · Forward token| SvcA
Gateway -->|5 · Forward token| SvcB
SvcA -.->|Verify roles locally| SvcA
SvcB -.->|Verify scopes locally| SvcB
Architect Insight
Never validate tokens only at the gateway — each service must validate the JWT signature itself. This prevents token forgery if the gateway is compromised or bypassed. Use a shared JWKS endpoint so all services fetch the public key.
Q: How do you secure service-to-service communication (inter-service auth)?
Why interviewers ask this: Lateral movement is one of the biggest microservices security risks. Tests knowledge of zero-trust and mTLS.
Answer
Service-to-service calls should be authenticated and encrypted. Three common patterns:
Option 1 — mTLS (Mutual TLS) ✅ Recommended for zero-trust
- Both client and server present certificates
- Each service has its own identity (SPIFFE/SVID)
- Automated with a service mesh (Istio, Linkerd)
Option 2 — Shared secret / API keys
- Simple but hard to rotate at scale
- Not recommended for production microservices
Option 3 — Service accounts with OAuth2 Client Credentials
- Service authenticates with Auth Server using
client_id+client_secret - Receives token scoped to service-to-service calls
- Token included in each request
graph LR
SvcA["Service A · Client cert"]
SvcB["Service B · Server cert"]
CA["Certificate Authority · Istio CA"]
Mesh["Service Mesh · Istio"]
CA -->|Issue cert| SvcA
CA -->|Issue cert| SvcB
SvcA -->|mTLS · Both verify each other| SvcB
Mesh -.->|Enforce policy| SvcA
Mesh -.->|Enforce policy| SvcB
With Istio PeerAuthentication:
apiVersion: security.istio.io/v1beta1
kind: PeerAuthentication
metadata:
name: default
namespace: production
spec:
mtls:
mode: STRICT # Reject all non-mTLS traffic
Architect Insight
In a zero-trust architecture, never assume traffic inside the cluster is safe. Apply mTLS everywhere and use network policies to limit which services can talk to which. Istio automates certificate rotation so operational overhead is low.
Q: What is CSRF? Why is it not needed for REST APIs? How do you configure CORS in Spring Boot?
Answer
CSRF (Cross-Site Request Forgery):
- Attack where malicious site tricks browser into sending authenticated request to your API
- CSRF protection is only needed when the browser automatically sends credentials (session cookies)
- Stateless REST APIs using JWT in
Authorizationheader are not vulnerable — the browser doesn't auto-send the header - Therefore: disable CSRF for REST APIs, but keep it for traditional form-based login apps
CORS (Cross-Origin Resource Sharing): Controls which origins are allowed to call your API from browsers.
@Configuration
public class CorsConfig {
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOrigins(List.of("https://app.mycompany.com")); // Restrict to known origins
config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE"));
config.setAllowedHeaders(List.of("Authorization", "Content-Type"));
config.setAllowCredentials(true);
config.setMaxAge(3600L);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/api/**", config);
return source;
}
}
CORS vs CSRF summary:
| CORS | CSRF | |
|---|---|---|
| Purpose | Controls browser cross-origin access | Prevents forged browser requests |
| Enforced by | Browser (preflight) | Server (token validation) |
| Needed for REST+JWT? | ✅ Yes | ❌ No |
| Needed for session-based? | ✅ Yes | ✅ Yes |
Q: How does Spring Cloud Gateway provide centralized authentication and route-level security?
Answer
Spring Cloud Gateway sits at the edge and can enforce:
- JWT validation before routing
- Role/scope-based route access
- Rate limiting per user/IP
- Request/response transformation
# application.yml
spring:
cloud:
gateway:
routes:
- id: admin-route
uri: lb://admin-service
predicates:
- Path=/api/admin/**
filters:
- name: RequestRateLimiter
args:
redis-rate-limiter.replenishRate: 10
redis-rate-limiter.burstCapacity: 20
- id: public-route
uri: lb://catalog-service
predicates:
- Path=/api/catalog/**
@Component
public class AuthGatewayFilter implements GlobalFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String token = exchange.getRequest().getHeaders()
.getFirst(HttpHeaders.AUTHORIZATION);
if (token == null || !token.startsWith("Bearer ")) {
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();
}
// Validate JWT and extract claims
// Reject or forward based on route requirements
return chain.filter(exchange);
}
}
Gateway security layers:
graph LR
Client["Client"]
GW["API Gateway"]
Auth["Auth Filter · JWT Validate"]
RateLimit["Rate Limiter · Redis"]
RouteA["Route · Admin Service"]
RouteB["Route · Public Service"]
Client -->|Request + Bearer token| GW
GW --> Auth
Auth -->|Valid· extract roles| RateLimit
RateLimit -->|/api/admin/ · ADMIN role| RouteA
RateLimit -->|/api/catalog/ · any| RouteB
Auth -->|Invalid| Client
Q: How do you implement Resilience4j circuit breaker, retry, and fallback?
Answer
Resilience4j replaces Hystrix and provides composable resilience patterns.
Dependency:
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-spring-boot3</artifactId>
</dependency>
Configuration:
resilience4j:
circuitbreaker:
instances:
paymentService:
registerHealthIndicator: true
slidingWindowSize: 10 # Last 10 calls
failureRateThreshold: 50 # Open if 50% fail
waitDurationInOpenState: 10s # Wait before half-open
permittedNumberOfCallsInHalfOpenState: 3
retry:
instances:
paymentService:
maxAttempts: 3
waitDuration: 500ms
exponentialBackoffMultiplier: 2 # 500ms, 1s, 2s
retryExceptions:
- org.springframework.web.client.HttpServerErrorException
timelimiter:
instances:
paymentService:
timeoutDuration: 2s
Service implementation:
@Service
public class PaymentClient {
@CircuitBreaker(name = "paymentService", fallbackMethod = "paymentFallback")
@Retry(name = "paymentService")
@TimeLimiter(name = "paymentService")
public CompletableFuture<PaymentResponse> processPayment(PaymentRequest request) {
return CompletableFuture.supplyAsync(() ->
restClient.post()
.uri("http://payment-service/api/pay")
.body(request)
.retrieve()
.body(PaymentResponse.class)
);
}
// Fallback — called when circuit is open or all retries exhausted
public CompletableFuture<PaymentResponse> paymentFallback(
PaymentRequest request, Exception ex) {
log.warn("Payment service unavailable, returning pending status", ex);
return CompletableFuture.completedFuture(
PaymentResponse.pending(request.getOrderId())
);
}
}
Circuit Breaker state machine:
graph LR
Closed["CLOSED · Normal operation"]
Open["OPEN · Fail fast · No calls"]
HalfOpen["HALF-OPEN · Test calls"]
Closed -->|Failure rate > 50%| Open
Open -->|Wait 10s| HalfOpen
HalfOpen -->|3 calls succeed| Closed
HalfOpen -->|Any failure| Open
Pattern composition order: TimeLimiter → CircuitBreaker → Retry → Fallback
Common Mistake
Don't use @Retry on non-idempotent operations like payment processing without idempotency keys. Retrying a POST that partially succeeded creates duplicate orders. Always design fallbacks to be idempotent.
Advanced Security
Q: What is OpenID Connect (OIDC)? How does it extend OAuth2?
Why interviewers ask this: OAuth2 and OIDC are frequently conflated. Tests precise understanding of what each protocol provides and how identity flows in microservices.
Answer
OAuth2 alone provides authorisation ("can this client access this resource?") but has no standard way to convey who the user is.
OpenID Connect (OIDC) is an identity layer built on top of OAuth2 that adds:
- An ID Token (JWT containing user identity claims:
sub,email,name) - A
/userinfoendpoint to fetch additional claims - Standardised scopes:
openid,profile,email - Discovery document at
/.well-known/openid-configuration
Flow comparison:
OAuth2 Authorization Code Flow:
1. Client redirects user to Auth Server with scope=read:orders
2. User logs in and consents
3. Auth Server returns authorization code
4. Client exchanges code → Access Token (opaque or JWT)
5. Client uses Access Token to call Resource Server API
Result: Client can access API, but doesn't know who the user is
OIDC on top of OAuth2:
Same steps 1-4, but scope includes "openid"
4b. Auth Server ALSO returns ID Token (JWT)
ID Token contains: sub (user ID), email, name, auth_time, iss, aud
Result: Client knows BOTH what it can access AND who the user is
Token types summary:
| Token | Purpose | Format | Who validates |
|---|---|---|---|
| Access Token | Authorise API calls | JWT or opaque | Resource Server |
| ID Token | Identify the user | Always JWT | Client application |
| Refresh Token | Obtain new access tokens | Opaque | Auth Server only |
Spring Boot OIDC login:
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.oauth2Login(oauth2 -> oauth2
.userInfoEndpoint(userInfo -> userInfo
.oidcUserService(oidcUserService()) // Process ID Token claims
)
.successHandler(authSuccessHandler())
)
.oauth2ResourceServer(rs -> rs.jwt(Customizer.withDefaults()));
return http.build();
}
}
// Extract user identity from ID Token
@GetMapping("/me")
public UserProfile getCurrentUser(@AuthenticationPrincipal OidcUser user) {
return new UserProfile(
user.getSubject(), // Stable user ID ("sub" claim)
user.getEmail(), // From ID Token or /userinfo
user.getFullName()
);
}
Architect Insight
Use the Access Token for API-to-API calls and the ID Token only in the client application to establish session identity. Never send ID Tokens to backend APIs — they are meant for the relying party (client) that requested the login, not for resource servers.
Q: What are Passkeys (WebAuthn/FIDO2)? How do they replace passwords?
Why interviewers ask this: Passkeys are rapidly replacing password-based auth for consumer and enterprise applications. Tests awareness of modern credential standards and their security properties.
Answer
Passkeys use public-key cryptography instead of passwords. The private key never leaves the user's device; the server stores only the public key.
Why passkeys are stronger than passwords:
| Attack Vector | Password | Passkey |
|---|---|---|
| Phishing | ✖ User can be tricked | ✔ Credential is origin-bound — can't be phished |
| Credential stuffing | ✖ Reused passwords exposed | ✔ Each key is unique per site |
| Server breach | ✖ Hashed passwords leaked | ✔ Public key only — useless without device |
| Brute force | ✖ Weak passwords guessable | ✔ Asymmetric key — not guessable |
| Man-in-the-middle | ✖ Password interceptable | ✔ Challenge-response — private key never transmitted |
Registration and authentication flow:
Registration (one-time, replaces "create password"):
1. Server sends a challenge
2. Device authenticator (Face ID, Touch ID, Windows Hello) generates
a new key pair: private key stored in device secure enclave, public key returned
3. Server stores: credentialId, publicKey, userId
✔ Private key never leaves the device
Authentication (replaces "enter password"):
1. Server sends a challenge
2. User approves via biometric/PIN on device
3. Device signs the challenge with the stored private key
4. Server verifies signature using stored public key
✔ No secret transmitted over the network
Spring Security WebAuthn (Spring Security 6.3+):
@Configuration
@EnableWebSecurity
public class PasskeySecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.formLogin(Customizer.withDefaults())
.webAuthn(webAuthn -> webAuthn
.rpName("My Application")
.rpId("myapp.com")
.allowedOrigins("https://myapp.com")
);
return http.build();
}
@Bean
public UserDetailsService userDetailsService() {
// Spring Security handles WebAuthn credential storage
return new InMemoryUserDetailsManager();
}
}
Passkey vs traditional MFA:
Traditional MFA: password + TOTP/SMS code
Two secrets to manage, both phishable
Passkey: biometric + device possession
Single gesture, phishing-resistant, no secret transmitted
Syncs across devices via iCloud Keychain / Google Password Manager
Q: What are the OWASP Top 10 risks for microservices? How do you mitigate them in Spring Boot?
Why interviewers ask this: Security must be built in, not bolted on. Tests systematic security awareness across the full attack surface of a distributed system.
Answer
| # | OWASP Risk | Microservices Context | Spring Boot Mitigation |
|---|---|---|---|
| A01 | Broken Access Control | Service A accessing Service B's data without authorisation | @PreAuthorize, method-level security, scope validation per endpoint |
| A02 | Cryptographic Failures | PII in logs, HTTP inter-service traffic, weak hashing | TLS everywhere (mTLS), AES-256 for PII fields, bcrypt for passwords |
| A03 | Injection | SQL, NoSQL, command injection via user input | Parameterised queries, JPA named parameters, input validation |
| A04 | Insecure Design | No rate limiting, missing idempotency, God service | API Gateway rate limiting, threat modelling in design phase |
| A05 | Security Misconfiguration | Actuator endpoints exposed publicly, debug logs in prod | management.endpoints.web.exposure.include=health,info only |
| A06 | Vulnerable Components | Outdated Spring Boot, Log4Shell in dependencies | Dependabot / Renovate, Trivy/Snyk CVE scanning in CI |
| A07 | Auth Failures | JWT not validated in downstream services, no token expiry | Short-lived tokens (15min), validate signature + claims in every service |
| A08 | Software Integrity | Unsigned Docker images, unverified dependencies | Image signing (Cosign), SBOM generation, checksum verification |
| A09 | Logging Failures | No audit trail, sensitive data in logs, no correlation | Structured logging with correlation IDs, never log PII/tokens |
| A10 | SSRF | Service calling arbitrary URLs from user input | Allowlist outbound URLs, block metadata endpoints (169.254.169.254) |
Most critical mitigations in code:
// A03: SQL Injection — ALWAYS use parameterised queries
// ❌ NEVER do this:
String query = "SELECT * FROM orders WHERE id = '" + orderId + "'";
jdbcTemplate.queryForObject(query, Order.class);
// ✔ Parameterised query:
jdbcTemplate.queryForObject(
"SELECT * FROM orders WHERE id = ?",
new Object[]{orderId},
Order.class
);
// A05: Lock down Actuator endpoints
// application.yml
management:
endpoints:
web:
exposure:
include: health,info # Never expose: env, beans, heapdump, threaddump in prod
endpoint:
health:
show-details: never # Don't leak DB/service topology
// A07: Validate JWT claims, not just signature
@Component
public class JwtValidator implements JwtAuthenticationConverter {
public AbstractAuthenticationToken convert(Jwt jwt) {
// Check expiry (Spring auto-validates, but be explicit)
if (jwt.getExpiresAt().isBefore(Instant.now())) {
throw new JwtExpiredException("Token expired");
}
// Check audience — prevent tokens from service A being used on service B
if (!jwt.getAudience().contains("order-service")) {
throw new JwtAudienceException("Invalid audience");
}
return super.convert(jwt);
}
}
// A09: Never log sensitive fields
@Slf4j
public class PaymentController {
public void processPayment(PaymentRequest req) {
// ❌ BAD: logs card number
log.info("Processing payment: {}", req);
// ✔ GOOD: log only safe fields
log.info("Processing payment: orderId={}, amount={}",
req.getOrderId(), req.getAmount());
}
}
Architect Insight
Build a security checklist into your PR template: parameterised queries, Actuator scope, no PII in logs, token audience validation, dependency scan passing. Security reviews catch one-time violations; automated checks in CI prevent them from ever merging.
Q: What is Zero Trust Architecture? How do you implement it in a microservices platform?
Why interviewers ask this: The traditional perimeter security model ("trust everything inside the network") fails in microservices. Tests understanding of defence-in-depth for distributed systems.
Answer
Zero Trust principle: "Never trust, always verify." Every request — whether it originates from outside the network or from another internal service — must be authenticated and authorised explicitly. The network perimeter is not a security boundary.
Why Zero Trust for microservices:
Traditional model:
External → Firewall → [Trusted Internal Network] → All services trust each other
Problem: One compromised service can access all others freely
Zero Trust model:
Every service-to-service call requires:
1. Mutual TLS (mTLS) — both sides prove identity via certificates
2. JWT/OAuth2 scope check — caller must have authorisation
3. Least-privilege policies — service A can only call what it needs
Implementation layers:
| Layer | Mechanism | Tool |
|---|---|---|
| Identity | mTLS certificates per service workload | Istio · SPIFFE/SPIRE · cert-manager |
| Authentication | JWT token validation on every call | Spring Security OAuth2 Resource Server |
| Authorization | Policy enforcement per endpoint | OPA (Open Policy Agent) · Kubernetes RBAC |
| Network | Deny-all default, explicit allow-list | Kubernetes NetworkPolicy |
| Secrets | No hardcoded credentials | Vault · AWS Secrets Manager |
Kubernetes NetworkPolicy (deny-all default):
# Deny all ingress by default
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: deny-all
namespace: orders
spec:
podSelector: {}
policyTypes:
- Ingress
- Egress
---
# Explicitly allow OrderService → PaymentService only
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-order-to-payment
namespace: payments
spec:
podSelector:
matchLabels:
app: payment-service
ingress:
- from:
- namespaceSelector:
matchLabels:
name: orders
podSelector:
matchLabels:
app: order-service
ports:
- port: 8080
Istio mTLS — automatic mutual authentication:
# Enforce strict mTLS across the mesh
apiVersion: security.istio.io/v1beta1
kind: PeerAuthentication
metadata:
name: default
namespace: istio-system
spec:
mtls:
mode: STRICT # Reject all non-mTLS traffic
graph LR
Client["External Client"]
GW["API Gateway\nmTLS termination · JWT validate"]
OrderSvc["Order Service\nJWT verify · scope check"]
PaySvc["Payment Service\nmTLS · JWT verify"]
Vault["Vault\nSecrets · Certs"]
Client -->|TLS + JWT| GW
GW -->|mTLS + JWT forward| OrderSvc
OrderSvc -->|mTLS + JWT| PaySvc
Vault -.->|Dynamic certs| OrderSvc
Vault -.->|Dynamic certs| PaySvc
style GW fill:#4ecdc4
style Vault fill:#ff6b6b
Architect Insight
Zero Trust is not a product you buy — it's a design posture. Start with three concrete steps: (1) enable mTLS in your service mesh, (2) add explicit NetworkPolicy deny-all with allow-list exceptions, (3) validate JWT claims in every service, not just at the gateway. Each step independently raises your security posture without requiring the others.
Q: How do you protect data in transit and at rest in a microservices architecture?
Why interviewers ask this: Data protection is a compliance baseline (PCI-DSS, HIPAA, GDPR). Tests understanding of encryption strategy across the full data lifecycle.
Answer
Data in transit — protect data moving between services, clients, and infrastructure:
| Hop | Protection | Implementation |
|---|---|---|
| Client → API Gateway | TLS 1.2+ (HTTPS) | Nginx · AWS ALB · Cloudflare |
| Service → Service (internal) | mTLS | Istio · Linkerd · cert-manager |
| Service → Database | TLS connection string | JDBC SSL param · sslmode=require |
| Service → Message Broker | TLS + SASL/SCRAM auth | Kafka ssl.* properties |
Spring Boot — enforce TLS for database connection:
spring:
datasource:
url: jdbc:postgresql://db-host:5432/orders?ssl=true&sslmode=require
hikari:
connection-init-sql: SET search_path TO orders
Data at rest — protect stored data in databases, object storage, and backups:
| Layer | Mechanism | Tool |
|---|---|---|
| Database | Transparent Data Encryption (TDE) | AWS RDS encryption · PostgreSQL pgcrypto |
| Field-level | Application-level encryption for PII | AES-256 before write |
| Object storage | Server-side encryption | S3 SSE-S3 · SSE-KMS |
| Secrets | Encrypted secret store | HashiCorp Vault · AWS Secrets Manager |
| Backups | Encrypted at backup time | Provider-managed or Velero with KMS |
Application-level field encryption for PII (Spring Boot):
@Converter
public class PiiEncryptionConverter implements AttributeConverter<String, String> {
@Autowired
private EncryptionService encryptionService;
@Override
public String convertToDatabaseColumn(String plaintext) {
return plaintext == null ? null : encryptionService.encrypt(plaintext);
}
@Override
public String convertToEntityAttribute(String ciphertext) {
return ciphertext == null ? null : encryptionService.decrypt(ciphertext);
}
}
@Entity
public class Customer {
@Id
private String id;
@Convert(converter = PiiEncryptionConverter.class)
private String ssn; // Encrypted at rest
@Convert(converter = PiiEncryptionConverter.class)
private String creditCardNumber;
}
Key management best practices:
1. Envelope encryption:
- Data Encryption Key (DEK): encrypts the actual data (rotated frequently)
- Key Encryption Key (KEK): encrypts the DEK (stored in KMS, rarely rotates)
- Compromise of DEK → re-encrypt data with new DEK; KEK stays safe
2. Key rotation:
- Automate rotation in AWS KMS / Vault
- Application must support decrypting with both old and new key during transition
3. Never:
- Store encryption keys in application config or environment variables
- Use the same key for all environments
- Log or expose plaintext of sensitive fields
Common Mistake
Encrypting the database but transmitting data over HTTP internally is a common gap. Attackers who gain access to internal network traffic (or a compromised pod) can intercept unencrypted service-to-service calls. Both layers must be protected — encryption at rest is not a substitute for mTLS.