Skip to content

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:

  1. Client authenticates with the Auth Server (Keycloak, Okta, Auth0)
  2. Auth Server issues a signed JWT access token
  3. Client sends token in Authorization: Bearer <token> header
  4. API Gateway validates the token signature and expiry
  5. 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 Authorization header 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
http.csrf(csrf -> csrf.disable()); // Safe for stateless JWT APIs

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 /userinfo endpoint 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.