|
| 1 | +# Hashicorp Vault authentication refactor |
| 2 | + |
| 3 | +## Decision |
| 4 | + |
| 5 | +Refactor the HashiCorp Vault extension and make it extensible with different methods of authentication. |
| 6 | + |
| 7 | +## Rationale |
| 8 | + |
| 9 | +The current implementation of the authentication in the HashiCorp Vault extension has no way to add new authentication methods. |
| 10 | +The full list of possible authentication methods can be found in the [HashiCorp Vault Documentation](https://developer.hashicorp.com/vault/docs/auth) |
| 11 | +Relevant examples are: |
| 12 | + |
| 13 | +* [Token auth](https://developer.hashicorp.com/vault/docs/auth/token) |
| 14 | +* [Kubernetes auth](https://developer.hashicorp.com/vault/docs/auth/kubernetes) |
| 15 | +* [AppRole auth](https://developer.hashicorp.com/vault/docs/auth/approle) |
| 16 | + |
| 17 | +It goes against the EDC extensibility model and needs to be remedied. |
| 18 | +Additionally, extracting the current implementation of the token authentication into its own extension will improve readability and maintainability of the code. |
| 19 | + |
| 20 | +## Approach |
| 21 | + |
| 22 | +The refactor will affect only the `vault-hashicorp` extension. |
| 23 | +It will create one additional extension called `vault-hashicorp-auth-tokenbased` that contains the token authentication and token renewal functionality. |
| 24 | + |
| 25 | +To allow an extensible authentication for HashiCorp Vault, the implementation will follow the registry pattern. |
| 26 | + |
| 27 | +### Hashicorp Vault Auth SPI |
| 28 | + |
| 29 | +For the proper organisation of the interfaces needed for the refactoring, a new module named `hashicorp-vault-auth-spi` is introduced in the `spi` directory. |
| 30 | + |
| 31 | +It will contain the following interfaces: |
| 32 | + |
| 33 | +* [Hashicorp Vault Auth Interface](#hashicorp-vault-auth-interface) |
| 34 | +* [Hashicorp Vault Auth Registry Interface](#hashicorp-vault-auth-registry) |
| 35 | + |
| 36 | +### Hashicorp Vault Auth Interface |
| 37 | + |
| 38 | +To implement a multitude of different authentication methods, an interface for the Hashicorp Vault authentication is needed. |
| 39 | +The sole common point between the authentication methods is the ability to provide a `client_token` that can be used to authenticate with HashiCorp Vault. |
| 40 | + |
| 41 | +```java |
| 42 | +public interface HashicorpVaultAuth { |
| 43 | + |
| 44 | + // The stored token is returned. |
| 45 | + String vaultToken(); |
| 46 | + |
| 47 | +} |
| 48 | +``` |
| 49 | + |
| 50 | +`HashicorpVaultAuth` implementations will be registered in `HashicorpVaultAuthRegistry` and used by the `HashicorpVaultClient` to receive the `client_token` for the request authentication. |
| 51 | +More on that in the sections [Hashicorp Vault Auth Implementation Registration](#Hashicorp-Vault-Auth-Registration) and [HashiCorp Vault Client](#HashiCorp-Vault-Client) |
| 52 | + |
| 53 | +### Haschicorp Vault Auth Service Extension |
| 54 | + |
| 55 | +For every authentication method, an implementation of the [Hashicorp Vault Auth interface](#hashicorp-vault-auth-interface) is needed. |
| 56 | +Each `HashicorpVaultAuth` implementation represents a different authentication method and is packaged inside its own service extension. |
| 57 | +In this way, it can easily be added/removed from the runtime and maintained in one place. |
| 58 | +Due to the possible differences in the needed configuration of different authentication methods, each Service Extension will need its own configuration values specific to the authentication method. |
| 59 | + |
| 60 | +### Simple Token Auth |
| 61 | + |
| 62 | +To keep the HashiCorp Vault Extension functional on its own for demo and testing purposes, `SimpleTokenAuth` is implemented. |
| 63 | +`SimpleTokenAuth` is a very rudimentary implementation of the [Hashicorp Vault Auth interface](#hashicorp-vault-auth-interface), that only stores a token and does not offer any refresh functionality. |
| 64 | +It is always added to the registry and will be used when `fallbackToken` is selected as authentication method, more on that in the [Configuration](#configuration) section. |
| 65 | + |
| 66 | +```java |
| 67 | +public class SimpleTokenAuth implements HashicorpVaultAuth { |
| 68 | + private String vaultToken; |
| 69 | + |
| 70 | + SimpleTokenAuth(String vaultToken) { |
| 71 | + this.vaultToken = vaultToken; |
| 72 | + } |
| 73 | + |
| 74 | + @Override |
| 75 | + public String vaultToken() { |
| 76 | + return vaultToken; |
| 77 | + } |
| 78 | +} |
| 79 | +``` |
| 80 | + |
| 81 | +### Hashicorp Vault Auth Registry |
| 82 | + |
| 83 | +In line with the registry pattern, `HashicorpVaultAuthRegistry` and `HashicorpVaultAuthRegistryImpl` are created. |
| 84 | +The `HashicorpVaultAuthRegistry` will be used to store one or more implementations of `HashicorpVaultAuth`, each representing a different authentication method. |
| 85 | +More on the usage of the `HashicorpVaultAuthRegistry` for registration in the [HashiCorp Vault Auth Registration](#hashicorp-vault-auth-registration) section. |
| 86 | +The `HashicorpVaultAuthRegistryImpl` is added to the constructor of `HashicorpVaultClient` and can then be used to retrieve the `vaultToken` for the Header creation, more in that in the [HashiCorp Vault Client](#hashicorp-vault-client) section. |
| 87 | + |
| 88 | +```java |
| 89 | +public interface HashicorpVaultAuthRegistry { |
| 90 | + |
| 91 | + void register(String method, HashicorpVaultAuth authImplementation); |
| 92 | + |
| 93 | + @NotNull |
| 94 | + HashicorpVaultAuth resolve(String method); |
| 95 | + |
| 96 | + boolean hasMethod(String method); |
| 97 | +} |
| 98 | +``` |
| 99 | + |
| 100 | +```java |
| 101 | +public class HashicorpVaultAuthRegistryImpl implements HashicorpVaultAuthRegistry { |
| 102 | + |
| 103 | + private final Map<String, HashicorpVaultAuth> services = new HashMap<>(); |
| 104 | + |
| 105 | + public HashicorpVaultAuthRegistryImpl() { |
| 106 | + } |
| 107 | + |
| 108 | + @Override |
| 109 | + public void register(String method, HashicorpVaultAuth service) { |
| 110 | + services.put(method, service); |
| 111 | + } |
| 112 | + |
| 113 | + @Override |
| 114 | + public @NotNull HashicorpVaultAuth resolve(String method) { |
| 115 | + if (services.get(method) == null){ |
| 116 | + throw new IllegalArgumentException(String.format("No authentication method registered under %s", method)); |
| 117 | + } |
| 118 | + return services.get(method); |
| 119 | + } |
| 120 | + |
| 121 | + @Override |
| 122 | + public boolean hasMethod(String method) { |
| 123 | + return services.containsKey(method); |
| 124 | + } |
| 125 | +} |
| 126 | +``` |
| 127 | + |
| 128 | +### Hashicorp Vault Auth Registration |
| 129 | + |
| 130 | +During the `initialize()` call, service extensions providing an auth method will register an instance of their `HashicorpVaultAuth` implementation in the `HashicorpVaultAuthRegistry`. |
| 131 | +The `HashicorpVaultAuthRegistry` is provided to the service extension through use of the provider pattern by the `HashicorpVaultExtension`. |
| 132 | + |
| 133 | +```java |
| 134 | +@Provider |
| 135 | +public HashicorpVaultAuthRegistry hashicorpVaultAuthRegistry() { |
| 136 | + return new HashicorpVaultAuthRegistryImpl(); |
| 137 | +} |
| 138 | +``` |
| 139 | + |
| 140 | +Inside the service extension providing an `HashicorpVaultAuth` implementation, the `HashicorpVaultAuthRegistry` is injected. |
| 141 | + |
| 142 | +```java |
| 143 | +@Inject |
| 144 | +private HashicorpVaultAuthRegistry hashicorpVaultAuthRegistry; |
| 145 | +``` |
| 146 | + |
| 147 | +The injected `HashicorpVaultAuthRegistry` is used to register the `HashicorpVaultAuth` implementation. |
| 148 | + |
| 149 | +```java |
| 150 | +@Override |
| 151 | +public void initialize(ServiceExtensionContext context) { |
| 152 | + var token = context.getSetting(VAULT_TOKEN, null); |
| 153 | + |
| 154 | + if (hashicorpVaultAuthRegistry.hasMethod("token-based")) { |
| 155 | + throw new EdcException("Authentication method token-based is already registered"); |
| 156 | + } |
| 157 | + |
| 158 | + hashicorpVaultAuthRegistry.register("token-based", HashicorpVaultTokenAuth(token)); |
| 159 | + |
| 160 | +} |
| 161 | +``` |
| 162 | + |
| 163 | +### Configuration |
| 164 | + |
| 165 | +A new config value is introduced to the HashiCorp Vault Extension named `edc.vault.hashicorp.auth.method`. |
| 166 | +`edc.vault.hashicorp.auth.method` governs which `HashicorpVaultAuth` implementation is used from `HashicorpVaultAuthRegistry` and is persisted in `HashicorpVaultSettings`. |
| 167 | + |
| 168 | +For testing and demo purposes, another setting called `edc.vault.hashicorp.auth.token.fallback` is introduced. |
| 169 | +In case, the HashiCorp Vault Extension is used on its own, this setting can be used to store a token for authentication without any refresh mechanism. |
| 170 | + |
| 171 | +### HashiCorp Vault Client |
| 172 | + |
| 173 | +Since the `HashicorpVaultClient` contains a lot of authentication logic, it will also go through a refactoring. |
| 174 | +The goal of the refactoring, is the removal of the token authentication logic from `HashicorpVaultClient` and to make the authentication logic interchangeable. |
| 175 | +`VaultAuthenticationRegistry` is passed to `HashicorpVaultClient` during creation by `HashicorpVaultServiceExtension`. |
| 176 | +`HashicorpVaultClient` will use `VaultAuthenticationRegistry` based on `edc.vault.hashicorp.auth.method` setting to fetch the `client_token` that is provided by the chosen `HashicorpVaultAuth` implementation. |
| 177 | +`client_token` will then be used to generate the Headers for the HTTP requests and to authenticate with HashiCorp Vault. |
| 178 | + |
| 179 | +Old `getHeaders()`: |
| 180 | + |
| 181 | +```java |
| 182 | +@NotNull |
| 183 | +private Headers getHeaders() { |
| 184 | + var headersBuilder = new Headers.Builder().add(VAULT_REQUEST_HEADER, Boolean.toString(true)); |
| 185 | + headersBuilder.add(VAULT_TOKEN_HEADER, settings.token()); |
| 186 | + return headersBuilder.build(); |
| 187 | +} |
| 188 | +``` |
| 189 | + |
| 190 | +New `getHeaders()`: |
| 191 | + |
| 192 | +```java |
| 193 | +@NotNull |
| 194 | +private Headers getHeaders() { |
| 195 | + var headersBuilder = new Headers.Builder().add(VAULT_REQUEST_HEADER, Boolean.toString(true)); |
| 196 | + headersBuilder.add(VAULT_TOKEN_HEADER, vaultAuthenticationRegistry.resolve(settings.getAuthMethod()).vaultToken()); |
| 197 | + return headersBuilder.build(); |
| 198 | +} |
| 199 | +``` |
| 200 | + |
| 201 | +An important change that was made, is the point at which the headers are generated. |
| 202 | +Previously, the headers were generated only once since the token was not liable to change. |
| 203 | +With the addition of numerous potential authentication mechanisms, it can no longer be guaranteed, that the token never changes during refresh. |
| 204 | +An example for this would be the kubernetes authentication method, where only short term tokens are produced each time. |
| 205 | +As such, `headers` are no longer saved as a variable inside the `HashicorpVaultClient` and `getHeaders()` is called instead to always fetch the newest token. |
| 206 | + |
| 207 | +### HashiCorp Vault Extension refactor |
| 208 | + |
| 209 | +The token based authentication logic is refactored and moved into its own extension named `HashiCorp Vault Token Auth`. |
| 210 | +This includes the token refresh functionality and will lead to a refactoring of the following classes: |
| 211 | + |
| 212 | +`HashicorpVaultExtension.java`: `initialize()` no longer will start the token refresh and `stop()` will be removed since the extension does not govern token refresh anymore. |
| 213 | + |
| 214 | +`HashicorpVaultClient.java`: `isTokenRenewable()` and `renewToken()` and their private methods will be moved to a new dedicated client in the Token Auth Extension. |
| 215 | + |
| 216 | +`HashicorpVaultSettings.java`: some settings are moved and two new ones are added as described in the sections [HashiCorp Vault Token Auth Extension Settings](#hashicorp-vault-token-auth-extension-settings) and [Configuration](#configuration). |
| 217 | + |
| 218 | +`HashicorpVaultTokenRenewTask.java`: is moved to the HashiCorp Vault Token Auth Extension in its entirety. |
| 219 | + |
| 220 | +The respective tests covering the moved functionality are also moved accordingly. |
| 221 | + |
| 222 | +### HashiCorp Vault Token Auth Extension |
| 223 | + |
| 224 | +The HashiCorp Vault Token Auth Extension will contain the token storage functionality and the token refresh functionality that has been removed from the HashiCorp Vault Extension. |
| 225 | + |
| 226 | +#### HashiCorp Vault Token Auth Extension Settings |
| 227 | + |
| 228 | +The following settings will be moved from the HashiCorp Vault Extension to th HashiCorp Vault Token Auth Extension. |
| 229 | + |
| 230 | +Duplicate setting from the HashiCorp Vault Extension, since the HashiCorp Vault Token Auth Extension also needs access to the vault. |
| 231 | +`edc.vault.hashicorp.url` |
| 232 | + |
| 233 | +Settings moved from the HashiCorp Vault Extension, that only concern the token storage and token refresh. |
| 234 | +`edc.vault.hashicorp.token` |
| 235 | +`edc.vault.hashicorp.token.scheduled-renew-enabled` |
| 236 | +`edc.vault.hashicorp.token.ttl` |
| 237 | +`edc.vault.hashicorp.token.renew-buffer` |
| 238 | + |
| 239 | +### HashiCorp Vault Health Extension |
| 240 | + |
| 241 | +The `get()` method of `HashicorpVaultHealthCheck` is currently using a merge of the response from the Hashicorp Vault Health API and the ability to renew the given token. |
| 242 | +After the refactor, the token renewal is no longer a part of the base `HashicorpVaultExtension`. |
| 243 | +As such the `get()` method is adjusted and will only return the response from the Hashicorp Vault Health API. |
0 commit comments