Spring Boot 3 Actuator Key Sanitize
@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 λ μλμ κ°μ΄ λ³κ²½λμμ΅λλ€.
@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;
}
}
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 μ½λλ₯Ό μ°Έκ³ νμ¬ κ΅¬ννλ©΄ λ κ² κ°μ΅λλ€.
@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"
}
}
}