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.yamlmanagement: 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"
}
}
}