Spring Boot 3 Actuator Key Sanitize

Spring Boot 2
@ConfigurationProperties("management.endpoint.env") public class EnvironmentEndpointProperties { private String[] keysToSanitize; public EnvironmentEndpointProperties() { } public String[] getKeysToSanitize() { return this.keysToSanitize; } public void setKeysToSanitize(String[] keysToSanitize) { this.keysToSanitize = keysToSanitize; } }

μŠ€ν”„λ§ λΆ€νŠΈ 2μ—μ„œ 앑좔에이터 μ—”λ“œν¬μΈνŠΈμ— λŒ€ν•΄ λ―Όκ°ν•œ 데이터가 λ…ΈμΆœλ˜λŠ” 것을 λ°©μ§€ν•˜κΈ° μœ„ν•΄μ„œkeys-to-sanitize 속성을 톡해 νŠΉμ • ν‚€ νŒ¨ν„΄μ— λŒ€ν•œ 데이터가 λ§ˆμŠ€ν‚Ή λ˜λ„λ‘ μ§€μ›ν–ˆμŠ΅λ‹ˆλ‹€. κ·ΈλŸ¬λ‚˜, μŠ€ν”„λ§ λΆ€νŠΈ 3 λΆ€ν„°λŠ” ν‚€ 기반이 μ•„λ‹ŒμΈμ¦ 및 μ—­ν• (Role) 기반의 Sanitizeλ₯Ό μˆ˜ν–‰ν•˜λŠ” κ²ƒμœΌλ‘œ λ³€κ²½λ˜μ—ˆμŠ΅λ‹ˆλ‹€. 이에 λŒ€ν•œ μ •λ³΄λŠ” λ§ˆμ΄κ·Έλ ˆμ΄μ…˜ κ°€μ΄λ“œμ˜ Actuator Endpoints Sanitization둜 κΈ°μž¬λ˜μ–΄ 있으며 EnvironmentEndpointProperties λŠ” μ•„λž˜μ™€ 같이 λ³€κ²½λ˜μ—ˆμŠ΅λ‹ˆλ‹€.

Spring Boot 3
@ConfigurationProperties("management.endpoint.env") public class EnvironmentEndpointProperties { private Show showValues; private final Set<String> roles; public EnvironmentEndpointProperties() { this.showValues = Show.NEVER; this.roles = new HashSet(); } public Show getShowValues() { return this.showValues; } public void setShowValues(Show showValues) { this.showValues = showValues; } public Set<String> getRoles() { return this.roles; } }
application.yaml
management: endpoints: web: exposure: include: - health - env endpoint: env: show-values: when_authorized roles: ADMIN

Customizing Sanitization

μŠ€ν”„λ§ λΆ€νŠΈ 3 μ—μ„œ 기쑴의 ν‚€ νŒ¨ν„΄ 기반의 Sanitize λ₯Ό μ μš©ν•˜κ³ μž ν•œλ‹€λ©΄ 곡식 λ¬Έμ„œμ˜ Customizing Sanitization에 따라SanitizingFunction μΈν„°νŽ˜μ΄μŠ€μ˜ κ΅¬ν˜„μ²΄λ₯Ό 빈으둜 등둝해야 ν•©λ‹ˆλ‹€. μŠ€ν”„λ§ λΆ€νŠΈ 2.7 λ²„μ „μ˜ Sanitizer μ½”λ“œλ₯Ό μ°Έκ³ ν•˜μ—¬ κ΅¬ν˜„ν•˜λ©΄ 될 것 κ°™μŠ΅λ‹ˆλ‹€.

ActuatorSanitizingFunction
@Component public class ActuatorSanitizingFunction implements SanitizingFunction { private static final String[] REGEX_PARTS = {"*", "$", "^", "+"}; private static final Set<String> DEFAULT_KEYS_TO_SANITIZE = new LinkedHashSet<>( Arrays.asList("password", "secret", "key", "token", ".*credentials.*", "vcap_services", "^vcap\\.services.*$", "sun.java.command", "^spring[._]application[._]json$")); private static final Set<String> URI_USERINFO_KEYS = new LinkedHashSet<>( Arrays.asList("uri", "uris", "url", "urls", "address", "addresses")); private static final Pattern URI_USERINFO_PATTERN = Pattern .compile("^\\[?[A-Za-z][A-Za-z0-9\\+\\.\\-]+://.+:(.*)@.+$"); private final List<Pattern> keysToSanitize = new ArrayList<>(); static { DEFAULT_KEYS_TO_SANITIZE.addAll(URI_USERINFO_KEYS); } public ActuatorSanitizingFunction( @Value("${management.endpoint.additionalKeysToSanitize:}") List<String> additionalKeysToSanitize) { addKeysToSanitize(DEFAULT_KEYS_TO_SANITIZE); addKeysToSanitize(URI_USERINFO_KEYS); addKeysToSanitize(additionalKeysToSanitize); } private Pattern getPattern(String value) { if (isRegex(value)) { return Pattern.compile(value, Pattern.CASE_INSENSITIVE); } return Pattern.compile(".*" + value + "$", Pattern.CASE_INSENSITIVE); } private boolean isRegex(String value) { for (String part : REGEX_PARTS) { if (value.contains(part)) { return true; } } return false; } @Override public SanitizableData apply(SanitizableData data) { if (data.getValue() == null) { return data; } for (Pattern pattern : keysToSanitize) { if (pattern.matcher(data.getKey()).matches()) { if (keyIsUriWithUserInfo(pattern)) { return data.withValue(sanitizeUris(data.getValue().toString())); } return data.withValue(SanitizableData.SANITIZED_VALUE); } } return data; } private void addKeysToSanitize(Collection<String> keysToSanitize) { for (String key : keysToSanitize) { this.keysToSanitize.add(getPattern(key)); } } private boolean keyIsUriWithUserInfo(Pattern pattern) { for (String uriKey : URI_USERINFO_KEYS) { if (pattern.matcher(uriKey).matches()) { return true; } } return false; } private Object sanitizeUris(String value) { return Arrays.stream(value.split(",")).map(this::sanitizeUri).collect(Collectors.joining(",")); } private String sanitizeUri(String value) { Matcher matcher = URI_USERINFO_PATTERN.matcher(value); String password = matcher.matches() ? matcher.group(1) : null; if (password != null) { return StringUtils.replace(value, ":" + password + "@", ":" + SanitizableData.SANITIZED_VALUE + "@"); } return value; } }

μœ„μ™€ 같이 SanitizingFunction μΈν„°νŽ˜μ΄μŠ€λ₯Ό κ΅¬ν˜„ν•˜κ²Œ 되면 κ΄€λ¦¬μž(ADMIN)둜 인증된 μ‚¬μš©μžκ°€/actuator/env μ—”λ“œν¬μΈνŠΈλ₯Ό ν˜ΈμΆœν•΄λ„ μ•„λž˜μ™€ κ°™μ΄λ―Όκ°ν•œ μ •λ³΄λŠ” λ§ˆμŠ€ν‚Ήλ˜μ–΄ 처리된 것을 확인할 수 μžˆμŠ΅λ‹ˆλ‹€. 이처럼 μ‹œμŠ€ν…œμ—μ„œ μ‚¬μš©λ˜λŠ” λ―Όκ°ν•œ μ •λ³΄λŠ” 보호될 수 μžˆλ„λ‘ λ³΄μ™„ν•©μ‹œλ‹€.

{
    "name": "Config resource 'class path resource [application.yml]' via location 'optional:classpath:'",
    "properties": {
        "management.endpoints.web.exposure.include[0]": {
            "value": "health",
            "origin": "class path resource [application.yml] - 6:13"
        },
        "management.endpoints.web.exposure.include[1]": {
            "value": "env",
            "origin": "class path resource [application.yml] - 7:13"
        },
        "management.endpoint.env.show-values": {
            "value": "always",
            "origin": "class path resource [application.yml] - 10:20"
        },
        "management.endpoint.env.roles": {
            "value": "ADMIN",
            "origin": "class path resource [application.yml] - 11:14"
        },
        "spring.datasource.password": {
            "value": "******",
            "origin": "class path resource [application.yml] - 14:15"
        }
    }
}

참고 자료