@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: ADMINCustomizing 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"
        }
    }
}