spring-cloud / spring-cloud-vault

Configuration Integration with HashiCorp Vault
http://cloud.spring.io/spring-cloud-vault/
Apache License 2.0
270 stars 151 forks source link

Vault Namespace Login for Secret Sharing in non Hierarchical Relationship #694

Open jschell96 opened 11 months ago

jschell96 commented 11 months ago

Problem Description With the release of Vault 1.13.0 it's possible to share secrets between namespaces without an hierachical relationship. See documentation. For Auth Methods (other than Token, e.g. Kubernetes) the Namespace is required for the login. For importing secrets without the namespace a full path is needed. If the namespace is set in the vault configuration the 'X-Vault-Namespace' header is set for every request. Therefore its not possible to read secrets with an full qualified path.

Desired Solution It would be nice to set the namespace for the login only.

  spring: 
     cloud:
       vault: 
          login-namespace: 'MY-DOMAIN-NAMESPACE'

So we could reference the secrets like:

 spring: 
     config:
       import: 
        - "optional:vault://'MY-DOMAIN-NAMESPACE/kv2/secret1"
        - "optional:vault://'OTHER-DOMAIN-NAMESPACE/kv2/other-secret"

Workaround In order to make this possible we found following workaround:

Register a custome WebClientFactory in the Application Class:

public static void main(String[] args) throws GeneralSecurityException, IOException {
        SpringApplication application = new SpringApplication(Application.class);
        application.addBootstrapRegistryInitializer(registry -> registry.register(WebClientFactory.class, CustomVaultSupplier .getWebClientFactorySupplier("MY-DOMAIN-NAMESPACE")));
        application.run(args);
    }

Creating an CustomVaultWebClientFactory:


public class CustomVaultWebClientFactory implements WebClientFactory {

    private final ClientHttpConnector connector;

    private final Function<ClientHttpConnector, WebClientBuilder> builderFunction;

    CustomVaultWebClientFactory(ClientHttpConnector connector, Function<ClientHttpConnector, WebClientBuilder> builderFunction) {
        this.connector = connector;
        this.builderFunction = builderFunction;
    }

    @Override
    public WebClient create(@Nullable Consumer<WebClientBuilder> customizer) {

        WebClientBuilder builder = builderFunction.apply(connector);

        if (customizer != null) {
            customizer.accept(builder);
        }

        return builder.build();
    }

}

Adding an ExchangeFilterFunction (Interceptor) to the (Kubernetes) auth method:


public class CustomVaultSupplier {

    public static BootstrapRegistry.InstanceSupplier<WebClientFactory> getWebClientFactorySupplier(String loginNamespace) {
        return context -> new CustomVaultWebClientFactory (context.get(VaultReactiveAutoConfiguration.ClientHttpConnectorWrapper.class).getConnector(), (connector) -> {
            VaultEndpoint endpoint = VaultEndpoint.create(context.get(VaultProperties.class).getHost(), context.get(VaultProperties.class).getPort());
            endpoint.setScheme(context.get(VaultProperties.class).getScheme());
            return WebClientBuilder.builder().filter(getKubernetesLoginNamespaceFilter(context.get(VaultProperties.class).getKubernetes().getKubernetesPath(), loginNamespace))
                    .endpointProvider(SimpleVaultEndpointProvider.of(endpoint));
        });
    }

    private static ExchangeFilterFunction getKubernetesLoginNamespaceFilter(String kubernetesPath, String loginNamespace) {
        return (clientRequest, nextFilter) -> {
            if (clientRequest.url().toString().contains(String.format("auth/%s/login", kubernetesPath))) {
                ClientRequest newRequest = ClientRequest.from(clientRequest).header("X-Vault-Namespace", loginNamespace).build();
                return nextFilter.exchange(newRequest);
            }
            return nextFilter.exchange(clientRequest);
        };
    }

}
mp911de commented 11 months ago

In Spring Cloud vault, we keep a single RestTemplateFactory/WebClientFactory instance that holds all configuration. If we update the config to set default headers, then headers are applied to all RestTemplate/WebClient instances produced from our factories.

We need to come up with a proper design approach without introducing too much complexity on our end and I expect this can take a while.

milansanjeev commented 10 months ago

@jschell96 @mp911de I also need to use different namespaces in both login and vault-get in Spring Vault. Can you please share some ref on how did you achieve this?