> ## Content Index
> Fetch the complete content index at: https://www.codyssey.tech/llms.txt
> Use this file to discover other available public pages before exploring further.

# 🔒 Guarding the Gates: Security Fundamentals for Microservices
- URL: https://www.codyssey.tech/guarding-the-gates/
- Published: 2026-05-27T08:00:00.000Z
- Updated: 2026-06-03T08:02:43.000Z
- Description: Security is either invisible and effective or visible and broken. This article covers the GUARD framework for Spring Security — SecurityFilterChain configuration, role-based access control, CORS policies, BCrypt password encoding, and a test security setup that doesn't weaken production.
- Author: Robert Marcel Saveanu
- Tags: Architecture & Infra, Security, Microservices, Spring Boot, #format-tutorial, #series-spring-boot, #syntax-highlight

📚 **Series Navigation:**  
← **Previous:** [Part 6 - Cache Me If You Can](https://www.codyssey.tech/cache-me-if-you-can/)  
👉 **You are here:** Part 7 - Guarding the Gates  
**Next:** [Part 8 - Fail Gracefully](https://www.codyssey.tech/fail-gracefully/) →

---

## 📋 Introduction

Security is the one thing nobody wants to think about until it's too late. You're building features, shipping code, making progress — and then someone discovers that your actuator endpoints are publicly accessible, your admin endpoints accept anonymous requests, and your API lets anyone delete every location in the database.

It's not that developers don't care about security. It's that security configuration in Spring Boot is genuinely confusing. The `SecurityFilterChain` API has evolved significantly across versions, tutorials are often outdated, and the difference between `authenticated()` and `hasRole("USER")` isn't always obvious until something goes wrong in production.

In this article, we'll demystify the Weather Microservice's security configuration. We'll see how it implements role-based access control, CORS policies, password encoding, and — critically — how it makes security testable without weakening it. ☕

---

## 🔒 The GUARD Framework: Five Pillars of Microservice Security

Meet **GUARD** — five principles for securing microservices:

| Letter | Principle             | What It Means                                         |
| ------ | --------------------- | ----------------------------------------------------- |
| **G**  | **Granular Rules**    | Access rules per HTTP method, not blanket allow/deny  |
| **U**  | **User Roles**        | Clear role hierarchy with least-privilege defaults    |
| **A**  | **Authentication**    | HTTP Basic auth with BCrypt password hashing          |
| **R**  | **Request Filtering** | CORS, CSRF, and header policies configured explicitly |
| **D**  | **Defense-in-depth**  | Production fail-fast on missing credentials           |

---

## 🔐 The Security Filter Chain

The heart of Spring Security is the `SecurityFilterChain`. Here's the Weather Microservice's complete configuration:

```java
@Configuration
@EnableWebSecurity
public class SecurityConfig {

  @Bean
  public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    return http.authorizeHttpRequests(auth -> auth
            // Actuator endpoints - require admin role
            .requestMatchers(EndpointRequest.toAnyEndpoint())
            .hasRole("ACTUATOR_ADMIN")

            // H2 Console - only in dev profile with authentication
            .requestMatchers("/h2-console/**")
            .hasRole("ADMIN")

            // API Documentation - public access
            .requestMatchers("/swagger-ui/**", "/swagger-ui.html",
                "/v3/api-docs/**", "/api-docs/**")
            .permitAll()

            // DELETE operations - admin only
            .requestMatchers(HttpMethod.DELETE, "/api/**")
            .hasRole("ADMIN")

            // POST/PUT operations - authenticated users
            .requestMatchers(HttpMethod.POST, "/api/**")
            .hasRole("USER")
            .requestMatchers(HttpMethod.PUT, "/api/**")
            .hasRole("USER")

            // GET operations - public read access
            .requestMatchers(HttpMethod.GET, "/api/**")
            .permitAll()

            // All other requests require authentication
            .anyRequest()
            .authenticated())
        .httpBasic(Customizer.withDefaults())
        .csrf(AbstractHttpConfigurer::disable)
        .cors(Customizer.withDefaults())
        .headers(headers ->
            headers.frameOptions(frame -> frame.sameOrigin()))
        .build();
  }
}

```

### Rule Order Matters

Spring Security evaluates rules **top to bottom, first match wins**. This ordering is deliberate:

```
1. Actuator → ACTUATOR_ADMIN   (most restricted, checked first)
2. H2 Console → ADMIN           (dev-only, restricted)
3. Swagger → permitAll           (documentation is public)
4. DELETE → ADMIN                (destructive operations need admin)
5. POST/PUT → USER              (write operations need auth)
6. GET → permitAll               (read operations are public)
7. anyRequest → authenticated    (catch-all safety net)

```

If the catch-all `anyRequest().authenticated()` were first, it would match everything and none of the specific rules would apply. Order is everything.

### RBAC by HTTP Method

The Weather Microservice uses HTTP method-based access control — a clean pattern for REST APIs:

| HTTP Method | Required Role   | Rationale                                       |
| ----------- | --------------- | ----------------------------------------------- |
| GET         | None (public)   | Read operations are safe, no state changes      |
| POST        | USER            | Creating resources requires authentication      |
| PUT         | USER            | Updating resources requires authentication      |
| DELETE      | ADMIN           | Destructive operations need elevated privileges |
| Actuator    | ACTUATOR\_ADMIN | Operations endpoints need separate admin role   |

This maps naturally to REST semantics. Anonymous users can browse weather data. Authenticated users can create and update locations. Only administrators can delete data or access operational endpoints.

> 💡 **Pro Tip:** Never give DELETE access to regular users in a REST API. It's irreversible and should always require elevated privileges.

---

## 👥 User Management and Password Security

### BCrypt with Strength 12

```java
@Bean
public PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder(12);
}

```

BCrypt strength 12 means 2^12 = 4,096 hashing rounds. This takes approximately 200ms per hash on modern hardware — fast enough for login, slow enough to make brute-force attacks impractical.

| BCrypt Strength | Rounds | Approx. Time | Use Case                     |
| --------------- | ------ | ------------ | ---------------------------- |
| 10              | 1,024  | \~50ms       | Default, adequate            |
| 12              | 4,096  | \~200ms      | Recommended for production   |
| 14              | 16,384 | \~800ms      | High security, slower logins |

### Three User Roles

```java
@Bean
public UserDetailsService userDetailsService() {
    UserDetails user = User.builder()
        .username(userUsername)
        .password(passwordEncoder().encode(userPassword))
        .roles("USER")
        .build();

    UserDetails admin = User.builder()
        .username(adminUsername)
        .password(passwordEncoder().encode(adminPassword))
        .roles("USER", "ADMIN")
        .build();

    UserDetails actuator = User.builder()
        .username(actuatorUsername)
        .password(passwordEncoder().encode(actuatorPassword))
        .roles("ACTUATOR_ADMIN")
        .build();

    return new InMemoryUserDetailsManager(user, admin, actuator);
}

```

Key design decisions:

- **Admin has both USER and ADMIN roles** — Admins can do everything users can, plus delete
- **Actuator has its own separate role** — Operations access is independent of application roles
- **Passwords are BCrypt-encoded** — Even in-memory, passwords are never stored in plain text

### Production Fail-Fast

```java
boolean isProduction = "prod".equals(System.getenv("SPRING_PROFILES_ACTIVE"));
if (isProduction
    && (userPassword == null || adminPassword == null || actuatorPassword == null)) {
  throw new IllegalStateException(
      "Production environment requires APP_USER_PASSWORD, APP_ADMIN_PASSWORD, "
      + "and APP_ACTUATOR_PASSWORD environment variables. "
      + "Never use default credentials in production!");
}

// Fall back to default passwords in dev/test only
if (userPassword == null) userPassword = "user123";

```

This is defense-in-depth for credentials:

- **In development**: Default passwords (`user123`, `admin123`) work out of the box
- **In production**: Missing passwords cause a startup failure with a clear error message
- **No silent fallbacks**: Production never silently uses default credentials

> 🔥 **Critical Insight:** This fail-fast approach is critical. Without it, a misconfigured production deployment could silently run with `user123` as the admin password. The `IllegalStateException` at startup is infinitely better than a security breach at runtime.

---

## 🌐 CORS Configuration

Cross-Origin Resource Sharing controls which frontend applications can call your API:

```java
@Bean
public CorsConfigurationSource corsConfigurationSource() {
    CorsConfiguration configuration = new CorsConfiguration();

    String allowedOriginsEnv = System.getenv("CORS_ALLOWED_ORIGINS");
    if (allowedOriginsEnv != null && !allowedOriginsEnv.isBlank()) {
      configuration.setAllowedOrigins(List.of(allowedOriginsEnv.split(",")));
    } else {
      configuration.setAllowedOrigins(
          List.of("http://localhost:3000", "http://localhost:4200",
              "http://localhost:8080"));
    }

    configuration.setAllowedMethods(
        List.of("GET", "POST", "PUT", "DELETE", "OPTIONS", "HEAD"));
    configuration.setAllowedHeaders(
        List.of("Authorization", "Content-Type", "Accept", "Origin",
            "X-Requested-With"));
    configuration.setExposedHeaders(
        List.of("Authorization", "Content-Disposition"));
    configuration.setAllowCredentials(true);
    configuration.setMaxAge(3600L);

    UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
    source.registerCorsConfiguration("/api/**", configuration);
    source.registerCorsConfiguration("/actuator/**", configuration);
    return source;
}

```

### Environment-Driven Origins

```java
String allowedOriginsEnv = System.getenv("CORS_ALLOWED_ORIGINS");
if (allowedOriginsEnv != null && !allowedOriginsEnv.isBlank()) {
    configuration.setAllowedOrigins(List.of(allowedOriginsEnv.split(",")));
}

```

In production: `CORS_ALLOWED_ORIGINS=https://app.example.com,https://admin.example.com`

In development: Falls back to common dev server ports (3000 for React, 4200 for Angular, 8080 for Spring).

### CORS Settings Explained

| Setting          | Value                                 | Purpose                                      |
| ---------------- | ------------------------------------- | -------------------------------------------- |
| allowedOrigins   | Environment-based                     | Controls which domains can call the API      |
| allowedMethods   | GET, POST, PUT, DELETE, OPTIONS, HEAD | HTTP methods the frontend can use            |
| allowedHeaders   | Authorization, Content-Type, etc.     | Headers the frontend can send                |
| exposedHeaders   | Authorization, Content-Disposition    | Headers the frontend can read from responses |
| allowCredentials | true                                  | Allows cookies and auth headers              |
| maxAge           | 3600s                                 | Browser caches preflight results for 1 hour  |

### Why CSRF Is Disabled

```java
.csrf(AbstractHttpConfigurer::disable)

```

CSRF protection is designed for browser-based form submissions with cookies. REST APIs typically use token-based authentication (Bearer tokens) or HTTP Basic auth, where CSRF attacks aren't applicable. Enabling CSRF on a REST API would break clients that don't send CSRF tokens (every non-browser client).

---

## 🧪 Making Security Testable

One of the hardest parts of security is testing. The Weather Microservice uses a `TestSecurityConfig` that relaxes security for integration tests:

```java
// TestSecurityConfig.java
@TestConfiguration
public class TestSecurityConfig {

  @Bean
  public SecurityFilterChain testFilterChain(HttpSecurity http) throws Exception {
    return http
        .authorizeHttpRequests(auth -> auth.anyRequest().permitAll())
        .httpBasic(Customizer.withDefaults())
        .csrf(AbstractHttpConfigurer::disable)
        .build();
  }

  @Bean
  public UserDetailsService testUserDetailsService() {
    // Simple users with "password" for all roles
    UserDetails user = User.builder()
        .username("user")
        .password(passwordEncoder().encode("password"))
        .roles("USER")
        .build();

    UserDetails admin = User.builder()
        .username("admin")
        .password(passwordEncoder().encode("password"))
        .roles("USER", "ADMIN")
        .build();

    return new InMemoryUserDetailsManager(user, admin);
  }
}

```

### Test Configuration Strategy

| Aspect        | Production Config            | Test Config             |
| ------------- | ---------------------------- | ----------------------- |
| Authorization | RBAC per HTTP method         | permitAll()             |
| Passwords     | BCrypt strength 12, env vars | Simple "password"       |
| Users         | 3 users with specific roles  | 2 users with test roles |
| CSRF          | Disabled (REST API)          | Disabled (same)         |

### Using @WithMockUser in Tests

```java
// WeatherControllerIntegrationTest.java
@WebMvcTest(WeatherController.class)
@Import({TestSecurityConfig.class, GlobalExceptionHandler.class})
@ActiveProfiles("test")
class WeatherControllerIntegrationTest {

  @Test
  @WithMockUser(roles = "USER")
  void getCurrentWeather_shouldReturnWeatherData() throws Exception {
      // Test with USER role
  }

  @Test
  @WithMockUser(roles = "ADMIN")
  void deleteLocation_shouldSucceedWithAdminRole() throws Exception {
      // Test with ADMIN role
  }
}

```

The `@WithMockUser` annotation creates a fake authenticated user without needing actual credentials. This lets you test authorization rules without the overhead of real authentication.

---

## 🔍 Security Layers in Practice

Here's how a request flows through the security stack:

```
Incoming Request: DELETE /api/locations/42

1. CORS Filter
   +- Origin allowed? → Continue
   +- Origin blocked? → 403 Forbidden

2. Security Filter Chain
   +- Match: DELETE /api/** → requires ADMIN role
   +- Authentication: HTTP Basic header present?
   |   +- Yes → Decode, verify password against BCrypt hash
   |   +- No → 401 Unauthorized
   +- Authorization: User has ADMIN role?
       +- Yes → Continue to controller
       +- No → 403 Forbidden

3. Controller
   +- locationService.deleteLocation(42)

```

Each layer adds a check:

- **CORS** — Is the caller's origin allowed?
- **Authentication** — Who is the caller?
- **Authorization** — Is the caller permitted to do this?

---

## ✅ Security Checklist

- \[ \] **RBAC by HTTP method** — GET is public, POST/PUT needs USER, DELETE needs ADMIN
- \[ \] **Actuator endpoints protected** — Separate ACTUATOR\_ADMIN role
- \[ \] **BCrypt strength 12** — Strong enough for production, fast enough for development
- \[ \] **Production fail-fast** — Missing passwords crash startup, never fall back to defaults
- \[ \] **CORS configured per environment** — `CORS_ALLOWED_ORIGINS` env var for production
- \[ \] **CSRF disabled for REST API** — Token-based auth doesn't need CSRF protection
- \[ \] **Swagger/OpenAPI publicly accessible** — Documentation shouldn't require auth
- \[ \] **TestSecurityConfig** for integration tests — Relaxed security without weakening production
- \[ \] **`@WithMockUser`** for role-based test scenarios
- \[ \] **Rule order verified** — Most specific rules first, catch-all last

---

## 🎓 Conclusion: Security Is a Feature, Not an Afterthought

Security done right is invisible to users and impenetrable to attackers. The key decisions:

1. **The GUARD framework** (Granular rules, User roles, Authentication, Request filtering, Defense-in-depth) guides security configuration
2. **`SecurityFilterChain`** with method-based authorization maps cleanly to REST API semantics
3. **Rule order matters** — most specific first, catch-all `anyRequest()` last
4. **BCrypt strength 12** provides strong password hashing with reasonable performance
5. **Production fail-fast** prevents deploying with default credentials
6. **CORS configuration** is environment-driven — dev-friendly defaults, production requires explicit origins
7. **`TestSecurityConfig`** separates test security from production security cleanly
8. **`@WithMockUser`** enables testing authorization without real authentication

Security isn't something you bolt on after the feature is done. It's woven into the configuration from day one. The Weather Microservice treats security as a first-class concern — with clear rules, sensible defaults, and an absolute refusal to compromise in production.

**Coming Next Week:**  
Part 8: Fail Gracefully - Error Handling, Validation, and the Art of Useful Errors 💥

---

📚 **Series Progress**

✅ Part 1: The Blueprint Before the Build  
✅ Part 2: Spring Boot Alchemy  
✅ Part 3: REST Assured  
✅ Part 4: The Data Foundation  
✅ Part 5: When the World Breaks  
✅ Part 6: Cache Me If You Can  
✅ Part 7: Guarding the Gates ← You just finished this!  
⬜ Part 8: Fail Gracefully  
⬜ Part 9: 10,000 Threads and a Dream  
⬜ Part 10: Can You See Me Now?  
⬜ Part 11: Trust, But Verify  
⬜ Part 12: Ship It  
⬜ Part 13: To Production and Beyond

---

*Happy coding, and remember — the only thing worse than no security is security you think you have but don't.* ☕