Search code examples
spring-bootspring-mvcspring-securityspring-boot-actuatorspring-boot-3

Actuator endpoints masking the values post Spring boot v3.2.1 migration


I am in the process of moving all my existing spring boot applications from version 2.7.4 to 3.2.1. We leverage actuator endpoints heavily for different purposes.

Post upgrade, I see that actuator endpoints are masking the values for security reasons for below endpoints

GET /actuator/env
GET /actuator/configprops

I am aware of this change in Spring Boot 3. As per official documentation, I have set the following properties to have proper control over the actuator endpoints' values

management.endpoint.env.show-values=when-authorized
management.endpoint.configprops.show-values=when-authorized

However, what if I still want to hide certain values even from authorized users from the above endpoints? it is possible to do so?


Solution

  • I found the solution to the above problem as listed here : https://github.com/spring-projects/spring-boot/issues/32156#issuecomment-1470804473

    we can achieve it programmatically by implementing SanitizingFunction as follows (example copied from above GitHub article):

    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.boot.actuate.endpoint.SanitizableData;
    import org.springframework.boot.actuate.endpoint.SanitizingFunction;
    import org.springframework.stereotype.Component;
    import org.springframework.util.StringUtils;
    
    import java.util.ArrayList;
    import java.util.Arrays;
    import java.util.Collection;
    import java.util.List;
    import java.util.Set;
    import java.util.regex.Matcher;
    import java.util.regex.Pattern;
    import java.util.stream.Collectors;
    
    @Component
    public class ActuatorSanitizer implements SanitizingFunction {
    
        private static final String[] REGEX_PARTS = {"*", "$", "^", "+"};
    
        private static final Set<String> DEFAULT_KEYS_TO_SANITIZE = Set.of(
                "password", "secret", "key", "token", ".*credentials.*", "vcap_services", "^vcap\\.services.*$", "sun.java.command", "^spring[._]application[._]json$"
        );
    
        private static final Set<String> URI_USERINFO_KEYS = Set.of(
                "uri", "uris", "url", "urls", "address", "addresses"
        );
    
        private static final Pattern URI_USERINFO_PATTERN = Pattern.compile("^\\[?[A-Za-z][A-Za-z0-9\\+\\.\\-]+://.+:(.*)@.+$");
    
        private List<Pattern> keysToSanitize = new ArrayList<>();
    
        public ActuatorSanitizer(@Value("${management.endpoint.additionalKeysToSanitize:}") List<String> additionalKeysToSanitize) {
            addKeysToSanitize(DEFAULT_KEYS_TO_SANITIZE);
            addKeysToSanitize(URI_USERINFO_KEYS);
            addKeysToSanitize(additionalKeysToSanitize);
        }
    
        @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 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;
        }
    
        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;
        }
    }
    

    And then add additional configurations for the keys you want to sanitize by setting:

     management.endpoint.additionalKeysToSanitize="my-value-1,my-value-2"