Repository: kroxylicious/kroxylicious
Pull request #791 (2023-12-06 16:06:19)

Making EnvelopeEncryptionFilterIT a template test so it can test HashiCorp Vault too
### Type of change

_Select the type of your PR_

- Enhancement / new feature

### Description

_Please describe your pull request_

### Additional Context

_Why are you making this pull request?_

### Checklist

_Please go through this checklist and make sure all applicable tasks have been done_

- [ ] Write tests
- [ ] Make sure all tests pass
- [ ] Review performance test results. Ensure that any degradations to performance numbers are understood and justified.
- [ ] Make sure all Sonar-Lint warnings are addressed or are justifiably ignored.
- [ ] Update documentation
- [ ] Reference relevant issue(s) and close them after merging
- [ ] For user facing changes, update CHANGELOG.md (remember to include changes affecting the API of the test artefacts too).


k-wall: @robobario @gracegrimwood @SamBarker this is still a work in progress, but I'd value some feedback.
sonarcloud[bot]: SonarCloud Quality Gate failed.&nbsp; &nbsp; [![Quality Gate failed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/failed-16px.png 'Quality Gate failed')](https://sonarcloud.io/dashboard?id=kroxylicious_kroxylicious&pullRequest=791)

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=791&resolved=false&types=BUG) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=791&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=791&resolved=false&types=BUG)  
[![Vulnerability](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability-16px.png 'Vulnerability')](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=791&resolved=false&types=VULNERABILITY) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=791&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=791&resolved=false&types=VULNERABILITY)  
[![Security Hotspot](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot-16px.png 'Security Hotspot')](https://sonarcloud.io/project/security_hotspots?id=kroxylicious_kroxylicious&pullRequest=791&resolved=false&types=SECURITY_HOTSPOT) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/security_hotspots?id=kroxylicious_kroxylicious&pullRequest=791&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=kroxylicious_kroxylicious&pullRequest=791&resolved=false&types=SECURITY_HOTSPOT)  
[![Code Smell](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell-16px.png 'Code Smell')](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=791&resolved=false&types=CODE_SMELL) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=791&resolved=false&types=CODE_SMELL) [0 Code Smells](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=791&resolved=false&types=CODE_SMELL)

[![60.0%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/60-16px.png '60.0%')](https://sonarcloud.io/component_measures?id=kroxylicious_kroxylicious&pullRequest=791&metric=new_coverage&view=list) [60.0% Coverage](https://sonarcloud.io/component_measures?id=kroxylicious_kroxylicious&pullRequest=791&metric=new_coverage&view=list)  
[![0.0%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/3-16px.png '0.0%')](https://sonarcloud.io/component_measures?id=kroxylicious_kroxylicious&pullRequest=791&metric=new_duplicated_lines_density&view=list) [0.0% Duplication](https://sonarcloud.io/component_measures?id=kroxylicious_kroxylicious&pullRequest=791&metric=new_duplicated_lines_density&view=list)



![idea](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/light_bulb-16px.png 'idea') Catch issues before they fail your Quality Gate with our IDE extension ![sonarlint](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/sonarlint-16px.png 'sonarlint') [SonarLint](https://www.sonarsource.com/products/sonarlint/features/connected-mode/?referrer=sonarcloud-welcome)
sonarcloud[bot]: Kudos, SonarCloud Quality Gate passed!&nbsp; &nbsp; [![Quality Gate passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/passed-16px.png 'Quality Gate passed')](https://sonarcloud.io/dashboard?id=kroxylicious_kroxylicious&pullRequest=791)

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=791&resolved=false&types=BUG) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=791&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=791&resolved=false&types=BUG)  
[![Vulnerability](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability-16px.png 'Vulnerability')](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=791&resolved=false&types=VULNERABILITY) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=791&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=791&resolved=false&types=VULNERABILITY)  
[![Security Hotspot](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot-16px.png 'Security Hotspot')](https://sonarcloud.io/project/security_hotspots?id=kroxylicious_kroxylicious&pullRequest=791&resolved=false&types=SECURITY_HOTSPOT) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/security_hotspots?id=kroxylicious_kroxylicious&pullRequest=791&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=kroxylicious_kroxylicious&pullRequest=791&resolved=false&types=SECURITY_HOTSPOT)  
[![Code Smell](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell-16px.png 'Code Smell')](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=791&resolved=false&types=CODE_SMELL) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=791&resolved=false&types=CODE_SMELL) [0 Code Smells](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=791&resolved=false&types=CODE_SMELL)

[![91.7%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/90-16px.png '91.7%')](https://sonarcloud.io/component_measures?id=kroxylicious_kroxylicious&pullRequest=791&metric=new_coverage&view=list) [91.7% Coverage](https://sonarcloud.io/component_measures?id=kroxylicious_kroxylicious&pullRequest=791&metric=new_coverage&view=list)  
[![0.0%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/3-16px.png '0.0%')](https://sonarcloud.io/component_measures?id=kroxylicious_kroxylicious&pullRequest=791&metric=new_duplicated_lines_density&view=list) [0.0% Duplication](https://sonarcloud.io/component_measures?id=kroxylicious_kroxylicious&pullRequest=791&metric=new_duplicated_lines_density&view=list)


sonarcloud[bot]: Kudos, SonarCloud Quality Gate passed!&nbsp; &nbsp; [![Quality Gate passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/passed-16px.png 'Quality Gate passed')](https://sonarcloud.io/dashboard?id=kroxylicious_kroxylicious&pullRequest=791)

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=791&resolved=false&types=BUG) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=791&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=791&resolved=false&types=BUG)  
[![Vulnerability](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability-16px.png 'Vulnerability')](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=791&resolved=false&types=VULNERABILITY) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=791&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=791&resolved=false&types=VULNERABILITY)  
[![Security Hotspot](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot-16px.png 'Security Hotspot')](https://sonarcloud.io/project/security_hotspots?id=kroxylicious_kroxylicious&pullRequest=791&resolved=false&types=SECURITY_HOTSPOT) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/security_hotspots?id=kroxylicious_kroxylicious&pullRequest=791&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=kroxylicious_kroxylicious&pullRequest=791&resolved=false&types=SECURITY_HOTSPOT)  
[![Code Smell](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell-16px.png 'Code Smell')](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=791&resolved=false&types=CODE_SMELL) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=791&resolved=false&types=CODE_SMELL) [0 Code Smells](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=791&resolved=false&types=CODE_SMELL)

[![91.7%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/90-16px.png '91.7%')](https://sonarcloud.io/component_measures?id=kroxylicious_kroxylicious&pullRequest=791&metric=new_coverage&view=list) [91.7% Coverage](https://sonarcloud.io/component_measures?id=kroxylicious_kroxylicious&pullRequest=791&metric=new_coverage&view=list)  
[![0.0%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/3-16px.png '0.0%')](https://sonarcloud.io/component_measures?id=kroxylicious_kroxylicious&pullRequest=791&metric=new_duplicated_lines_density&view=list) [0.0% Duplication](https://sonarcloud.io/component_measures?id=kroxylicious_kroxylicious&pullRequest=791&metric=new_duplicated_lines_density&view=list)


sonarcloud[bot]: Kudos, SonarCloud Quality Gate passed!&nbsp; &nbsp; [![Quality Gate passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/passed-16px.png 'Quality Gate passed')](https://sonarcloud.io/dashboard?id=kroxylicious_kroxylicious&pullRequest=791)

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=791&resolved=false&types=BUG) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=791&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=791&resolved=false&types=BUG)  
[![Vulnerability](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability-16px.png 'Vulnerability')](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=791&resolved=false&types=VULNERABILITY) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=791&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=791&resolved=false&types=VULNERABILITY)  
[![Security Hotspot](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot-16px.png 'Security Hotspot')](https://sonarcloud.io/project/security_hotspots?id=kroxylicious_kroxylicious&pullRequest=791&resolved=false&types=SECURITY_HOTSPOT) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/security_hotspots?id=kroxylicious_kroxylicious&pullRequest=791&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=kroxylicious_kroxylicious&pullRequest=791&resolved=false&types=SECURITY_HOTSPOT)  
[![Code Smell](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell-16px.png 'Code Smell')](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=791&resolved=false&types=CODE_SMELL) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=791&resolved=false&types=CODE_SMELL) [0 Code Smells](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=791&resolved=false&types=CODE_SMELL)

[![91.7%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/90-16px.png '91.7%')](https://sonarcloud.io/component_measures?id=kroxylicious_kroxylicious&pullRequest=791&metric=new_coverage&view=list) [91.7% Coverage](https://sonarcloud.io/component_measures?id=kroxylicious_kroxylicious&pullRequest=791&metric=new_coverage&view=list)  
[![0.0%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/3-16px.png '0.0%')](https://sonarcloud.io/component_measures?id=kroxylicious_kroxylicious&pullRequest=791&metric=new_duplicated_lines_density&view=list) [0.0% Duplication](https://sonarcloud.io/component_measures?id=kroxylicious_kroxylicious&pullRequest=791&metric=new_duplicated_lines_density&view=list)


sonarcloud[bot]: Kudos, SonarCloud Quality Gate passed!&nbsp; &nbsp; [![Quality Gate passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/passed-16px.png 'Quality Gate passed')](https://sonarcloud.io/dashboard?id=kroxylicious_kroxylicious&pullRequest=791)

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=791&resolved=false&types=BUG) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=791&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=791&resolved=false&types=BUG)  
[![Vulnerability](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability-16px.png 'Vulnerability')](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=791&resolved=false&types=VULNERABILITY) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=791&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=791&resolved=false&types=VULNERABILITY)  
[![Security Hotspot](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot-16px.png 'Security Hotspot')](https://sonarcloud.io/project/security_hotspots?id=kroxylicious_kroxylicious&pullRequest=791&resolved=false&types=SECURITY_HOTSPOT) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/security_hotspots?id=kroxylicious_kroxylicious&pullRequest=791&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=kroxylicious_kroxylicious&pullRequest=791&resolved=false&types=SECURITY_HOTSPOT)  
[![Code Smell](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell-16px.png 'Code Smell')](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=791&resolved=false&types=CODE_SMELL) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=791&resolved=false&types=CODE_SMELL) [0 Code Smells](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=791&resolved=false&types=CODE_SMELL)

[![91.7%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/90-16px.png '91.7%')](https://sonarcloud.io/component_measures?id=kroxylicious_kroxylicious&pullRequest=791&metric=new_coverage&view=list) [91.7% Coverage](https://sonarcloud.io/component_measures?id=kroxylicious_kroxylicious&pullRequest=791&metric=new_coverage&view=list)  
[![0.0%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/3-16px.png '0.0%')](https://sonarcloud.io/component_measures?id=kroxylicious_kroxylicious&pullRequest=791&metric=new_duplicated_lines_density&view=list) [0.0% Duplication](https://sonarcloud.io/component_measures?id=kroxylicious_kroxylicious&pullRequest=791&metric=new_duplicated_lines_density&view=list)


sonarcloud[bot]: Kudos, SonarCloud Quality Gate passed!&nbsp; &nbsp; [![Quality Gate passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/passed-16px.png 'Quality Gate passed')](https://sonarcloud.io/dashboard?id=kroxylicious_kroxylicious&pullRequest=791)

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=791&resolved=false&types=BUG) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=791&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=791&resolved=false&types=BUG)  
[![Vulnerability](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability-16px.png 'Vulnerability')](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=791&resolved=false&types=VULNERABILITY) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=791&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=791&resolved=false&types=VULNERABILITY)  
[![Security Hotspot](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot-16px.png 'Security Hotspot')](https://sonarcloud.io/project/security_hotspots?id=kroxylicious_kroxylicious&pullRequest=791&resolved=false&types=SECURITY_HOTSPOT) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/security_hotspots?id=kroxylicious_kroxylicious&pullRequest=791&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=kroxylicious_kroxylicious&pullRequest=791&resolved=false&types=SECURITY_HOTSPOT)  
[![Code Smell](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell-16px.png 'Code Smell')](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=791&resolved=false&types=CODE_SMELL) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=791&resolved=false&types=CODE_SMELL) [0 Code Smells](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=791&resolved=false&types=CODE_SMELL)

[![91.7%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/90-16px.png '91.7%')](https://sonarcloud.io/component_measures?id=kroxylicious_kroxylicious&pullRequest=791&metric=new_coverage&view=list) [91.7% Coverage](https://sonarcloud.io/component_measures?id=kroxylicious_kroxylicious&pullRequest=791&metric=new_coverage&view=list)  
[![0.0%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/3-16px.png '0.0%')](https://sonarcloud.io/component_measures?id=kroxylicious_kroxylicious&pullRequest=791&metric=new_duplicated_lines_density&view=list) [0.0% Duplication](https://sonarcloud.io/component_measures?id=kroxylicious_kroxylicious&pullRequest=791&metric=new_duplicated_lines_density&view=list)


sonarcloud[bot]: Kudos, SonarCloud Quality Gate passed!&nbsp; &nbsp; [![Quality Gate passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/passed-16px.png 'Quality Gate passed')](https://sonarcloud.io/dashboard?id=kroxylicious_kroxylicious&pullRequest=791)

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=791&resolved=false&types=BUG) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=791&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=791&resolved=false&types=BUG)  
[![Vulnerability](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability-16px.png 'Vulnerability')](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=791&resolved=false&types=VULNERABILITY) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=791&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=791&resolved=false&types=VULNERABILITY)  
[![Security Hotspot](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot-16px.png 'Security Hotspot')](https://sonarcloud.io/project/security_hotspots?id=kroxylicious_kroxylicious&pullRequest=791&resolved=false&types=SECURITY_HOTSPOT) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/security_hotspots?id=kroxylicious_kroxylicious&pullRequest=791&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=kroxylicious_kroxylicious&pullRequest=791&resolved=false&types=SECURITY_HOTSPOT)  
[![Code Smell](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell-16px.png 'Code Smell')](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=791&resolved=false&types=CODE_SMELL) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=791&resolved=false&types=CODE_SMELL) [0 Code Smells](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=791&resolved=false&types=CODE_SMELL)

[![91.7%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/90-16px.png '91.7%')](https://sonarcloud.io/component_measures?id=kroxylicious_kroxylicious&pullRequest=791&metric=new_coverage&view=list) [91.7% Coverage](https://sonarcloud.io/component_measures?id=kroxylicious_kroxylicious&pullRequest=791&metric=new_coverage&view=list)  
[![0.0%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/3-16px.png '0.0%')](https://sonarcloud.io/component_measures?id=kroxylicious_kroxylicious&pullRequest=791&metric=new_duplicated_lines_density&view=list) [0.0% Duplication](https://sonarcloud.io/component_measures?id=kroxylicious_kroxylicious&pullRequest=791&metric=new_duplicated_lines_density&view=list)


k-wall: @SamBarker @robobario Could you take another look?  I'm hoping that this is approaching landable and we can cut the next release very soon.
sonarcloud[bot]: ## [![Quality Gate Failed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-failed-20px.png 'Quality Gate Failed')](https://sonarcloud.io/dashboard?id=kroxylicious_kroxylicious&pullRequest=791) **Quality Gate failed**  
Failed conditions

[10.6% Coverage on New Code](https://sonarcloud.io/component_measures?id=kroxylicious_kroxylicious&pullRequest=791&metric=new_coverage&view=list) (required ≥ 76%)  
  
[See analysis details on SonarCloud](https://sonarcloud.io/dashboard?id=kroxylicious_kroxylicious&pullRequest=791)


sonarcloud[bot]: ## [![Quality Gate Failed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-failed-20px.png 'Quality Gate Failed')](https://sonarcloud.io/dashboard?id=kroxylicious_kroxylicious&pullRequest=791) **Quality Gate failed**  
Failed conditions

[10.6% Coverage on New Code](https://sonarcloud.io/component_measures?id=kroxylicious_kroxylicious&pullRequest=791&metric=new_coverage&view=list) (required ≥ 76%)  
  
[See analysis details on SonarCloud](https://sonarcloud.io/dashboard?id=kroxylicious_kroxylicious&pullRequest=791)


sonarcloud[bot]: ## [![Quality Gate Failed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-failed-20px.png 'Quality Gate Failed')](https://sonarcloud.io/dashboard?id=kroxylicious_kroxylicious&pullRequest=791) **Quality Gate failed**  
Failed conditions

[10.6% Coverage on New Code](https://sonarcloud.io/component_measures?id=kroxylicious_kroxylicious&pullRequest=791&metric=new_coverage&view=list) (required ≥ 76%)  
  
[See analysis details on SonarCloud](https://sonarcloud.io/dashboard?id=kroxylicious_kroxylicious&pullRequest=791)


sonarcloud[bot]: ## [![Quality Gate Failed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-failed-20px.png 'Quality Gate Failed')](https://sonarcloud.io/dashboard?id=kroxylicious_kroxylicious&pullRequest=791) **Quality Gate failed**  
Failed conditions

[10.6% Coverage on New Code](https://sonarcloud.io/component_measures?id=kroxylicious_kroxylicious&pullRequest=791&metric=new_coverage&view=list) (required ≥ 76%)  
  
[See analysis details on SonarCloud](https://sonarcloud.io/dashboard?id=kroxylicious_kroxylicious&pullRequest=791)


sonarcloud[bot]: ## [![Quality Gate Failed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-failed-20px.png 'Quality Gate Failed')](https://sonarcloud.io/dashboard?id=kroxylicious_kroxylicious&pullRequest=791) **Quality Gate failed**  
Failed conditions

[10.6% Coverage on New Code](https://sonarcloud.io/component_measures?id=kroxylicious_kroxylicious&pullRequest=791&metric=new_coverage&view=list) (required ≥ 76%)  
  
[See analysis details on SonarCloud](https://sonarcloud.io/dashboard?id=kroxylicious_kroxylicious&pullRequest=791)


sonarcloud[bot]: ## [![Quality Gate Failed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-failed-20px.png 'Quality Gate Failed')](https://sonarcloud.io/dashboard?id=kroxylicious_kroxylicious&pullRequest=791) **Quality Gate failed**  
Failed conditions

[54.4% Coverage on New Code](https://sonarcloud.io/component_measures?id=kroxylicious_kroxylicious&pullRequest=791&metric=new_coverage&view=list) (required ≥ 76%)  
[C Reliability Rating on New Code](https://sonarcloud.io/dashboard?id=kroxylicious_kroxylicious&pullRequest=791) (required ≥ A)  
  
[See analysis details on SonarCloud](https://sonarcloud.io/dashboard?id=kroxylicious_kroxylicious&pullRequest=791)



![idea](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/light_bulb-16px.png 'idea') Catch issues before they fail your Quality Gate with our IDE extension ![SonarLint](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/sonarlint-16px.png 'SonarLint') [SonarLint](https://www.sonarsource.com/products/sonarlint/features/connected-mode/?referrer=pull-request)
sonarcloud[bot]: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=kroxylicious_kroxylicious&pullRequest=791) **Quality Gate passed**  
The SonarCloud Quality Gate passed, but some issues were introduced.

[7 New issues](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=791&resolved=false&inNewCodePeriod=true)  
[0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=kroxylicious_kroxylicious&pullRequest=791&resolved=false&inNewCodePeriod=true)  
[76.2% Coverage on New Code](https://sonarcloud.io/component_measures?id=kroxylicious_kroxylicious&pullRequest=791&metric=new_coverage&view=list)  
[0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=kroxylicious_kroxylicious&pullRequest=791&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarCloud](https://sonarcloud.io/dashboard?id=kroxylicious_kroxylicious&pullRequest=791)


sonarcloud[bot]: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=kroxylicious_kroxylicious&pullRequest=791) **Quality Gate passed**  
The SonarCloud Quality Gate passed, but some issues were introduced.

[7 New issues](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=791&resolved=false&inNewCodePeriod=true)  
[0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=kroxylicious_kroxylicious&pullRequest=791&resolved=false&inNewCodePeriod=true)  
[76.2% Coverage on New Code](https://sonarcloud.io/component_measures?id=kroxylicious_kroxylicious&pullRequest=791&metric=new_coverage&view=list)  
[0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=kroxylicious_kroxylicious&pullRequest=791&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarCloud](https://sonarcloud.io/dashboard?id=kroxylicious_kroxylicious&pullRequest=791)


sonarcloud[bot]: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=kroxylicious_kroxylicious&pullRequest=791) **Quality Gate passed**  
The SonarCloud Quality Gate passed, but some issues were introduced.

[7 New issues](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=791&resolved=false&inNewCodePeriod=true)  
[0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=kroxylicious_kroxylicious&pullRequest=791&resolved=false&inNewCodePeriod=true)  
[76.2% Coverage on New Code](https://sonarcloud.io/component_measures?id=kroxylicious_kroxylicious&pullRequest=791&metric=new_coverage&view=list)  
[0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=kroxylicious_kroxylicious&pullRequest=791&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarCloud](https://sonarcloud.io/dashboard?id=kroxylicious_kroxylicious&pullRequest=791)


sonarcloud[bot]: ## [![Quality Gate Failed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-failed-20px.png 'Quality Gate Failed')](https://sonarcloud.io/dashboard?id=kroxylicious_kroxylicious&pullRequest=791) **Quality Gate failed**  
Failed conditions

[74.6% Coverage on New Code](https://sonarcloud.io/component_measures?id=kroxylicious_kroxylicious&pullRequest=791&metric=new_coverage&view=list) (required ≥ 76%)  
  
[See analysis details on SonarCloud](https://sonarcloud.io/dashboard?id=kroxylicious_kroxylicious&pullRequest=791)


sonarcloud[bot]: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=kroxylicious_kroxylicious&pullRequest=791) **Quality Gate passed**  
Kudos, no new issues were introduced!

[0 New issues](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=791&resolved=false&inNewCodePeriod=true)  
[0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=kroxylicious_kroxylicious&pullRequest=791&resolved=false&inNewCodePeriod=true)  
[77.6% Coverage on New Code](https://sonarcloud.io/component_measures?id=kroxylicious_kroxylicious&pullRequest=791&metric=new_coverage&view=list)  
[0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=kroxylicious_kroxylicious&pullRequest=791&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarCloud](https://sonarcloud.io/dashboard?id=kroxylicious_kroxylicious&pullRequest=791)


sonarcloud[bot]: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=kroxylicious_kroxylicious&pullRequest=791) **Quality Gate passed**  
Kudos, no new issues were introduced!

[0 New issues](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=791&resolved=false&inNewCodePeriod=true)  
[0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=kroxylicious_kroxylicious&pullRequest=791&resolved=false&inNewCodePeriod=true)  
[77.6% Coverage on New Code](https://sonarcloud.io/component_measures?id=kroxylicious_kroxylicious&pullRequest=791&metric=new_coverage&view=list)  
[0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=kroxylicious_kroxylicious&pullRequest=791&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarCloud](https://sonarcloud.io/dashboard?id=kroxylicious_kroxylicious&pullRequest=791)



Patch:
diff --git a/kroxylicious-bom/pom.xml b/kroxylicious-bom/pom.xml
index 1126d552..6505d021 100644
--- a/kroxylicious-bom/pom.xml
+++ b/kroxylicious-bom/pom.xml
@@ -155,6 +155,24 @@
 
             <!-- testing dependencies -->
             <!-- note scope is *not* set at this level -->
+            <dependency>
+                <groupId>io.kroxylicious</groupId>
+                <artifactId>kroxylicious-kms-test-support</artifactId>
+                <version>${project.version}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>io.kroxylicious</groupId>
+                <artifactId>kroxylicious-kms-provider-hashicorp-vault-test-support</artifactId>
+                <version>${project.version}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>io.kroxylicious</groupId>
+                <artifactId>kroxylicious-kms-provider-kroxylicious-inmemory-test-support</artifactId>
+                <version>${project.version}</version>
+            </dependency>
+
             <dependency>
                 <groupId>io.kroxylicious</groupId>
                 <artifactId>kroxylicious-filter-test-support</artifactId>
diff --git a/kroxylicious-filters/kroxylicious-encryption/src/main/java/io/kroxylicious/filter/encryption/EnvelopeEncryption.java b/kroxylicious-filters/kroxylicious-encryption/src/main/java/io/kroxylicious/filter/encryption/EnvelopeEncryption.java
index 93684f1b..dd6d587a 100644
--- a/kroxylicious-filters/kroxylicious-encryption/src/main/java/io/kroxylicious/filter/encryption/EnvelopeEncryption.java
+++ b/kroxylicious-filters/kroxylicious-encryption/src/main/java/io/kroxylicious/filter/encryption/EnvelopeEncryption.java
@@ -23,6 +23,8 @@ import edu.umd.cs.findbugs.annotations.NonNull;
 
 /**
  * A {@link FilterFactory} for {@link EnvelopeEncryptionFilter}.
+ * @param <K> The key reference
+ * @param <E> The type of encrypted DEK
  */
 @Plugin(configType = EnvelopeEncryption.Config.class)
 public class EnvelopeEncryption<K, E> implements FilterFactory<EnvelopeEncryption.Config, EnvelopeEncryption.Config> {
diff --git a/kroxylicious-filters/kroxylicious-encryption/src/main/java/io/kroxylicious/filter/encryption/TemplateKekSelector.java b/kroxylicious-filters/kroxylicious-encryption/src/main/java/io/kroxylicious/filter/encryption/TemplateKekSelector.java
index 9685c814..5b3272b3 100644
--- a/kroxylicious-filters/kroxylicious-encryption/src/main/java/io/kroxylicious/filter/encryption/TemplateKekSelector.java
+++ b/kroxylicious-filters/kroxylicious-encryption/src/main/java/io/kroxylicious/filter/encryption/TemplateKekSelector.java
@@ -11,6 +11,7 @@ import java.util.Map;
 import java.util.Objects;
 import java.util.Set;
 import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionException;
 import java.util.concurrent.CompletionStage;
 import java.util.regex.Pattern;
 
@@ -57,12 +58,11 @@ public class TemplateKekSelector<K> implements KekSelectorService<TemplateKekSel
                     .map(
                             topicName -> kms.resolveAlias(evaluateTemplate(topicName))
                                     .exceptionallyCompose(e -> {
-                                        if (e instanceof UnknownAliasException) {
+                                        if (e instanceof UnknownAliasException
+                                                || (e instanceof CompletionException ce && ce.getCause() instanceof UnknownAliasException)) {
                                             return CompletableFuture.completedFuture(null);
                                         }
-                                        else {
-                                            return CompletableFuture.failedFuture(e);
-                                        }
+                                        return CompletableFuture.failedFuture(e);
                                     })
                                     .thenApply(kekId -> new Pair<>(topicName, kekId)))
                     .toList();
diff --git a/kroxylicious-kms-provider-hashicorp-vault/pom.xml b/kroxylicious-kms-provider-hashicorp-vault/pom.xml
index c7263546..1f636718 100644
--- a/kroxylicious-kms-provider-hashicorp-vault/pom.xml
+++ b/kroxylicious-kms-provider-hashicorp-vault/pom.xml
@@ -22,10 +22,6 @@
     <packaging>jar</packaging>
     <name>Hashicorp Vault KMS</name>
     <description>A KMS provider backed by a remote instance of Hashicorp Vault</description>
-    <properties>
-        <testcontainers-vault.version>1.19.3</testcontainers-vault.version>
-    </properties>
-
     <dependencies>
         <dependency>
             <groupId>io.kroxylicious</groupId>
diff --git a/kroxylicious-kms-provider-hashicorp-vault/src/main/java/io/kroxylicious/kms/provider/hashicorp/vault/VaultKms.java b/kroxylicious-kms-provider-hashicorp-vault/src/main/java/io/kroxylicious/kms/provider/hashicorp/vault/VaultKms.java
index cdc1ba1f..4cca0041 100644
--- a/kroxylicious-kms-provider-hashicorp-vault/src/main/java/io/kroxylicious/kms/provider/hashicorp/vault/VaultKms.java
+++ b/kroxylicious-kms-provider-hashicorp-vault/src/main/java/io/kroxylicious/kms/provider/hashicorp/vault/VaultKms.java
@@ -15,6 +15,7 @@ import java.util.Base64;
 import java.util.Map;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.CompletionStage;
+import java.util.function.Function;
 import java.util.function.Supplier;
 
 import javax.crypto.SecretKey;
@@ -31,6 +32,7 @@ import io.kroxylicious.kms.service.DekPair;
 import io.kroxylicious.kms.service.Kms;
 import io.kroxylicious.kms.service.KmsException;
 import io.kroxylicious.kms.service.Serde;
+import io.kroxylicious.kms.service.UnknownAliasException;
 import io.kroxylicious.kms.service.UnknownKeyException;
 
 import edu.umd.cs.findbugs.annotations.NonNull;
@@ -81,7 +83,7 @@ public class VaultKms implements Kms<String, VaultEdek> {
                 .build();
 
         return vaultClient.sendAsync(request, statusHandler(kekRef, new JsonBodyHandler<VaultResponse<DataKeyData>>(new TypeReference<>() {
-        })))
+        }), UnknownKeyException::new))
                 .thenApply(HttpResponse::body)
                 .thenApply(Supplier::get)
                 .thenApply(VaultResponse::data)
@@ -109,7 +111,7 @@ public class VaultKms implements Kms<String, VaultEdek> {
                 .build();
 
         return vaultClient.sendAsync(request, statusHandler(edek.kekRef(), new JsonBodyHandler<VaultResponse<DecryptData>>(new TypeReference<>() {
-        }))).thenApply(HttpResponse::body)
+        }), UnknownKeyException::new)).thenApply(HttpResponse::body)
                 .thenApply(Supplier::get)
                 .thenApply(VaultResponse::data)
                 .thenApply(data -> new SecretKeySpec(Base64.getDecoder().decode(data.plaintext()), AES_KEY_ALGO));
@@ -140,7 +142,7 @@ public class VaultKms implements Kms<String, VaultEdek> {
                 .build();
 
         return vaultClient.sendAsync(request, statusHandler(alias, new JsonBodyHandler<VaultResponse<ReadKeyData>>(new TypeReference<>() {
-        })))
+        }), UnknownAliasException::new))
                 .thenApply(HttpResponse::body)
                 .thenApply(Supplier::get)
                 .thenApply(VaultResponse::data)
@@ -160,10 +162,10 @@ public class VaultKms implements Kms<String, VaultEdek> {
                 .header("Accept", "application/json");
     }
 
-    private static <T> HttpResponse.BodyHandler<T> statusHandler(String keyRef, HttpResponse.BodyHandler<T> handler) {
+    private static <T> HttpResponse.BodyHandler<T> statusHandler(String keyRef, HttpResponse.BodyHandler<T> handler, Function<String, KmsException> notFound) {
         return r -> {
             if (r.statusCode() == 404 || r.statusCode() == 400) {
-                throw new UnknownKeyException("key '%s' is not found.".formatted(keyRef));
+                throw notFound.apply("key '%s' is not found.".formatted(keyRef));
             }
             else if (r.statusCode() != 200) {
                 throw new KmsException("fail to retrieve key '%s', HTTP status code %d.".formatted(keyRef, r.statusCode()));
diff --git a/pom.xml b/pom.xml
index 88ce66d9..1a185d7c 100644
--- a/pom.xml
+++ b/pom.xml
@@ -69,6 +69,7 @@
         <maven.core.version>3.9.6</maven.core.version>
         <awaitility.version>4.2.0</awaitility.version>
         <commons.io.version>2.15.1</commons.io.version>
+        <testcontainers-vault.version>1.19.3</testcontainers-vault.version>
     </properties>
 
     <name>Parent POM</name>
@@ -304,6 +305,11 @@
                 <artifactId>api</artifactId>
                 <version>${strimzi.version}</version>
             </dependency>
+            <dependency>
+                <groupId>org.testcontainers</groupId>
+                <artifactId>vault</artifactId>
+                <version>${testcontainers-vault.version}</version>
+            </dependency>
             <dependency>
                 <groupId>org.apache.maven.enforcer</groupId>
                 <artifactId>enforcer-api</artifactId>
@@ -341,8 +347,11 @@
 
     <modules>
         <module>kroxylicious-kms</module>
+        <module>kroxylicious-kms-test-support</module>
         <module>kroxylicious-kms-provider-kroxylicious-inmemory</module>
+        <module>kroxylicious-kms-provider-kroxylicious-inmemory-test-support</module>
         <module>kroxylicious-kms-provider-hashicorp-vault</module>
+        <module>kroxylicious-kms-provider-hashicorp-vault-test-support</module>
         <module>kroxylicious-bom</module>
         <module>kroxylicious-api</module>
         <module>kroxylicious-app</module>


Test patch:
diff --git a/kroxylicious-filters/kroxylicious-encryption/src/test/java/io/kroxylicious/filter/encryption/TemplateKekSelectorTest.java b/kroxylicious-filters/kroxylicious-encryption/src/test/java/io/kroxylicious/filter/encryption/TemplateKekSelectorTest.java
index 75e1aaac..946f26be 100644
--- a/kroxylicious-filters/kroxylicious-encryption/src/test/java/io/kroxylicious/filter/encryption/TemplateKekSelectorTest.java
+++ b/kroxylicious-filters/kroxylicious-encryption/src/test/java/io/kroxylicious/filter/encryption/TemplateKekSelectorTest.java
@@ -6,7 +6,10 @@
 
 package io.kroxylicious.filter.encryption;
 
+import java.time.Duration;
 import java.util.Set;
+import java.util.UUID;
+import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.ExecutionException;
 
 import org.junit.jupiter.api.Test;
@@ -14,39 +17,79 @@ import org.junit.jupiter.api.Test;
 import io.kroxylicious.kms.provider.kroxylicious.inmemory.InMemoryKms;
 import io.kroxylicious.kms.provider.kroxylicious.inmemory.UnitTestingKmsService;
 import io.kroxylicious.kms.service.Kms;
+import io.kroxylicious.kms.service.KmsException;
+import io.kroxylicious.kms.service.UnknownAliasException;
 
 import edu.umd.cs.findbugs.annotations.NonNull;
 
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertNull;
-import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
 
 class TemplateKekSelectorTest {
 
     @Test
     void shouldRejectUnknownPlaceholders() {
-        var e = assertThrows(IllegalArgumentException.class, () -> getSelector(null, "foo-${topicId}-bar"));
-        assertEquals("Unknown template parameter: topicId", e.getMessage());
+        assertThatThrownBy(() -> getSelector(null, "foo-${topicId}-bar"))
+                .isInstanceOf(IllegalArgumentException.class)
+                .hasMessage("Unknown template parameter: topicId");
     }
 
     @Test
-    void shouldResolveWhenAliasExists() throws ExecutionException, InterruptedException {
-        InMemoryKms kms = UnitTestingKmsService.newInstance().buildKms(new UnitTestingKmsService.Config());
+    void shouldResolveWhenAliasExists() {
+        var kms = UnitTestingKmsService.newInstance().buildKms(new UnitTestingKmsService.Config());
         var selector = getSelector(kms, "topic-${topicName}");
 
         var kek = kms.generateKey();
         kms.createAlias(kek, "topic-my-topic");
-        var map = selector.selectKek(Set.of("my-topic")).toCompletableFuture().get();
-        assertEquals(kek, map.get("my-topic"));
+        var map = selector.selectKek(Set.of("my-topic")).toCompletableFuture().join();
+        assertThat(map)
+                .hasSize(1)
+                .containsEntry("my-topic", kek);
     }
 
     @Test
-    void shouldThrowWhenAliasDoesNotExist() throws ExecutionException, InterruptedException {
-        InMemoryKms kms = UnitTestingKmsService.newInstance().buildKms(new UnitTestingKmsService.Config());
+    void shouldNotThrowWhenAliasDoesNotExist() {
+        var kms = UnitTestingKmsService.newInstance().buildKms(new UnitTestingKmsService.Config());
         var selector = getSelector(kms, "topic-${topicName}");
 
+        var map = selector.selectKek(Set.of("my-topic")).toCompletableFuture().join();
+        assertThat(map)
+                .hasSize(1)
+                .containsEntry("my-topic", null);
+    }
+
+    @Test
+    void shouldNotThrowWhenAliasDoesNotExist_UnknownAliasExceptionWrappedInCompletionException() throws ExecutionException, InterruptedException {
+        var kms = mock(InMemoryKms.class);
+        var result = CompletableFuture.completedFuture(null)
+                .<UUID> thenApply((u) -> {
+                    // this exception will be wrapped by a CompletionException
+                    throw new UnknownAliasException("mock alias exception");
+                });
+        when(kms.resolveAlias(anyString())).thenReturn(result);
+        var selector = getSelector(kms, "topic-${topicName}");
         var map = selector.selectKek(Set.of("my-topic")).toCompletableFuture().get();
-        assertNull(map.get("my-topic"));
+        assertThat(map)
+                .hasSize(1)
+                .containsEntry("my-topic", null);
+    }
+
+    @Test
+    void serviceExceptionsArePropagated() {
+        var kms = mock(InMemoryKms.class);
+        var result = CompletableFuture.<UUID> failedFuture(new KmsException("bang!"));
+        when(kms.resolveAlias(anyString())).thenReturn(result);
+
+        var selector = getSelector(kms, "topic-${topicName}");
+        var stage = selector.selectKek(Set.of("my-topic"));
+        assertThat(stage)
+                .isCompletedExceptionally()
+                .failsWithin(Duration.ZERO)
+                .withThrowableThat()
+                .withCauseInstanceOf(KmsException.class);
     }
 
     @NonNull
diff --git a/kroxylicious-integration-tests/pom.xml b/kroxylicious-integration-tests/pom.xml
index 25c7dac1..cbb44bbf 100644
--- a/kroxylicious-integration-tests/pom.xml
+++ b/kroxylicious-integration-tests/pom.xml
@@ -38,6 +38,11 @@
             <artifactId>kroxylicious-integration-test-support</artifactId>
             <scope>test</scope>
         </dependency>
+        <dependency>
+            <groupId>io.kroxylicious</groupId>
+            <artifactId>kroxylicious-kms-test-support</artifactId>
+            <scope>test</scope>
+        </dependency>
         <dependency>
             <groupId>io.kroxylicious</groupId>
             <artifactId>kroxylicious-encryption</artifactId>
@@ -45,7 +50,12 @@
         </dependency>
         <dependency>
             <groupId>io.kroxylicious</groupId>
-            <artifactId>kroxylicious-kms-provider-kroxylicious-inmemory</artifactId>
+            <artifactId>kroxylicious-kms-provider-kroxylicious-inmemory-test-support</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>io.kroxylicious</groupId>
+            <artifactId>kroxylicious-kms-provider-hashicorp-vault-test-support</artifactId>
             <scope>test</scope>
         </dependency>
         <dependency>
@@ -91,6 +101,16 @@
             <artifactId>log4j-slf4j2-impl</artifactId>
             <scope>test</scope>
         </dependency>
+        <dependency>
+            <groupId>org.junit.jupiter</groupId>
+            <artifactId>junit-jupiter-api</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.junit.jupiter</groupId>
+            <artifactId>junit-jupiter-engine</artifactId>
+            <scope>test</scope>
+        </dependency>
         <dependency>
             <groupId>org.junit.jupiter</groupId>
             <artifactId>junit-jupiter-params</artifactId>
diff --git a/kroxylicious-integration-tests/src/test/java/io/kroxylicious/proxy/encryption/EnvelopeEncryptionFilterIT.java b/kroxylicious-integration-tests/src/test/java/io/kroxylicious/proxy/encryption/EnvelopeEncryptionFilterIT.java
index f767168b..ba8f4197 100644
--- a/kroxylicious-integration-tests/src/test/java/io/kroxylicious/proxy/encryption/EnvelopeEncryptionFilterIT.java
+++ b/kroxylicious-integration-tests/src/test/java/io/kroxylicious/proxy/encryption/EnvelopeEncryptionFilterIT.java
@@ -9,13 +9,8 @@ package io.kroxylicious.proxy.encryption;
 import java.time.Duration;
 import java.util.List;
 import java.util.Map;
-import java.util.Optional;
-import java.util.UUID;
 import java.util.concurrent.TimeUnit;
-import java.util.stream.Stream;
 
-import org.apache.kafka.clients.admin.Admin;
-import org.apache.kafka.clients.admin.NewTopic;
 import org.apache.kafka.clients.consumer.Consumer;
 import org.apache.kafka.clients.consumer.ConsumerConfig;
 import org.apache.kafka.clients.consumer.ConsumerRecord;
@@ -24,139 +19,106 @@ import org.apache.kafka.clients.producer.KafkaProducer;
 import org.apache.kafka.clients.producer.ProducerConfig;
 import org.apache.kafka.clients.producer.ProducerRecord;
 import org.apache.kafka.common.TopicPartition;
-import org.junit.jupiter.api.Test;
+import org.assertj.core.api.InstanceOfAssertFactories;
+import org.junit.jupiter.api.TestTemplate;
 import org.junit.jupiter.api.extension.ExtendWith;
 
+import io.kroxylicious.filter.encryption.EnvelopeEncryption;
 import io.kroxylicious.filter.encryption.TemplateKekSelector;
 import io.kroxylicious.kms.provider.kroxylicious.inmemory.InMemoryKms;
-import io.kroxylicious.kms.provider.kroxylicious.inmemory.IntegrationTestingKmsService;
+import io.kroxylicious.kms.service.TestKmsFacade;
 import io.kroxylicious.proxy.config.FilterDefinition;
 import io.kroxylicious.proxy.config.FilterDefinitionBuilder;
 import io.kroxylicious.testing.kafka.api.KafkaCluster;
 import io.kroxylicious.testing.kafka.common.ClientConfig;
 import io.kroxylicious.testing.kafka.junit5ext.KafkaClusterExtension;
+import io.kroxylicious.testing.kafka.junit5ext.Topic;
 
-import edu.umd.cs.findbugs.annotations.NonNull;
-
-import static io.kroxylicious.test.tester.KroxyliciousConfigUtils.DEFAULT_VIRTUAL_CLUSTER;
 import static io.kroxylicious.test.tester.KroxyliciousConfigUtils.proxy;
 import static io.kroxylicious.test.tester.KroxyliciousTesters.kroxyliciousTester;
 import static org.assertj.core.api.Assertions.assertThat;
-import static org.testcontainers.shaded.org.awaitility.Awaitility.await;
+import static org.assertj.core.api.Assumptions.assumeThatCode;
 
 @ExtendWith(KafkaClusterExtension.class)
+@ExtendWith(EnvelopeEncryptionTestInvocationContextProvider.class)
 class EnvelopeEncryptionFilterIT {
 
-    @Test
-    void roundTrip(KafkaCluster cluster) throws Exception {
-        var kmsId = UUID.randomUUID();
-        var kms = provisionKms(kmsId);
-        generateKekWithAlias(kms);
+    private static final String TEMPLATE_KEK_SELECTOR_PATTERN = "${topicName}";
+    private static final String HELLO_WORLD = "hello world";
+    private static final String HELLO_SECRET = "hello secret";
+
+    @TestTemplate
+    void roundTrip(KafkaCluster cluster, Topic topic, TestKmsFacade<?, ?, ?> testKmsFacade) throws Exception {
+        var testKekManager = testKmsFacade.getTestKekManager();
+        testKekManager.generateKek(topic.name());
 
         var builder = proxy(cluster);
 
-        builder.addToFilters(encryptionFilterDefinition(kmsId));
+        builder.addToFilters(buildEncryptionFilterDefinition(testKmsFacade));
 
         try (var tester = kroxyliciousTester(builder);
-                var admin = tester.admin();
                 var producer = tester.producer();
                 var consumer = tester.consumer()) {
 
-            String topic = tester.createTopic(DEFAULT_VIRTUAL_CLUSTER);
-
-            await().atMost(Duration.ofSeconds(5)).until(() -> admin.listTopics().namesToListings().get(),
-                    n -> n.containsKey(topic));
+            producer.send(new ProducerRecord<>(topic.name(), HELLO_WORLD)).get(5, TimeUnit.SECONDS);
 
-            var message = "hello world";
-            producer.send(new ProducerRecord<>(topic, message)).get(5, TimeUnit.SECONDS);
-
-            consumer.subscribe(List.of(topic));
+            consumer.subscribe(List.of(topic.name()));
             var records = consumer.poll(Duration.ofSeconds(2));
             assertThat(records.iterator())
                     .toIterable()
                     .singleElement()
                     .extracting(ConsumerRecord::value)
-                    .isEqualTo(message);
+                    .isEqualTo(HELLO_WORLD);
         }
     }
 
     // This ensures the decrypt-ability guarantee, post kek rotation
-    @Test
-    void decryptionAfterKekRotation(KafkaCluster cluster, Admin admin) throws Exception {
-        var kmsId = UUID.randomUUID();
-        var kms = provisionKms(kmsId);
-        generateKekWithAlias(kms);
-
-        var topicName = UUID.randomUUID().toString();
-        admin.createTopics(List.of(new NewTopic(topicName, Optional.empty(), Optional.empty()))).all().get(5, TimeUnit.SECONDS);
-        await().atMost(Duration.ofSeconds(5)).until(() -> admin.listTopics().namesToListings().get(),
-                n -> n.containsKey(topicName));
+    @TestTemplate
+    void decryptionAfterKekRotation(KafkaCluster cluster, Topic topic, TestKmsFacade<?, ?, ?> testKmsFacade) throws Exception {
+        var testKekManager = testKmsFacade.getTestKekManager();
+        testKekManager.generateKek(topic.name());
 
         var builder = proxy(cluster);
-        builder.addToFilters(encryptionFilterDefinition(kmsId));
+        builder.addToFilters(buildEncryptionFilterDefinition(testKmsFacade));
 
         var messageBeforeKeyRotation = "hello world, old key";
         var messageAfterKeyRotation = "hello world, new key";
         try (var tester = kroxyliciousTester(builder);
                 var producer = tester.producer()) {
-            producer.send(new ProducerRecord<>(topicName, messageBeforeKeyRotation)).get(5, TimeUnit.SECONDS);
-        }
-
-        // Now do the Kek rotation
-        generateKekWithAlias(kms);
-
-        builder.addToFilters(0, encryptionFilterDefinition(kmsId));
+            producer.send(new ProducerRecord<>(topic.name(), messageBeforeKeyRotation)).get(5, TimeUnit.SECONDS);
 
-        try (var tester = kroxyliciousTester(builder);
-                var producer = tester.producer();
-                var consumer = tester.consumer()) {
+            // Now do the Kek rotation
+            testKekManager.rotateKek(topic.name());
 
-            producer.send(new ProducerRecord<>(topicName, messageAfterKeyRotation)).get(5, TimeUnit.SECONDS);
+            producer.send(new ProducerRecord<>(topic.name(), messageAfterKeyRotation)).get(5, TimeUnit.SECONDS);
 
-            consumer.subscribe(List.of(topicName));
-            var records = consumer.poll(Duration.ofSeconds(2));
-            assertThat(records.iterator())
-                    .toIterable()
-                    .extracting(ConsumerRecord::value)
-                    .containsExactly(messageBeforeKeyRotation, messageAfterKeyRotation);
+            try (var consumer = tester.consumer()) {
+                consumer.subscribe(List.of(topic.name()));
+                var records = consumer.poll(Duration.ofSeconds(2));
+                assertThat(records.iterator())
+                        .toIterable()
+                        .extracting(ConsumerRecord::value)
+                        .containsExactly(messageBeforeKeyRotation, messageAfterKeyRotation);
+            }
         }
-
-        IntegrationTestingKmsService.delete(kmsId.toString());
-    }
-
-    @NonNull
-    private InMemoryKms provisionKms(UUID kmsId) {
-        return IntegrationTestingKmsService.newInstance().buildKms(new IntegrationTestingKmsService.Config(kmsId.toString()));
-    }
-
-    private UUID generateKekWithAlias(InMemoryKms kms) {
-        var kekId = kms.generateKey();
-        kms.createAlias(kekId, "all");
-        return kekId;
     }
 
-    @Test
-    void topicRecordsAreUnreadableOnServer(KafkaCluster cluster, KafkaConsumer<String, String> directConsumer) throws Exception {
-        var kmsId = UUID.randomUUID();
-        var kms = provisionKms(kmsId);
-        generateKekWithAlias(kms);
+    @TestTemplate
+    void topicRecordsAreUnreadableOnServer(KafkaCluster cluster, Topic topic, KafkaConsumer<String, String> directConsumer, TestKmsFacade<?, ?, ?> testKmsFacade)
+            throws Exception {
+        var testKekManager = testKmsFacade.getTestKekManager();
+        testKekManager.generateKek(topic.name());
 
         var builder = proxy(cluster);
-        builder.addToFilters(encryptionFilterDefinition(kmsId));
+        builder.addToFilters(buildEncryptionFilterDefinition(testKmsFacade));
 
         try (var tester = kroxyliciousTester(builder);
-                var admin = tester.admin();
                 var producer = tester.producer()) {
 
-            String topic = tester.createTopic(DEFAULT_VIRTUAL_CLUSTER);
-
-            await().atMost(Duration.ofSeconds(5)).until(() -> admin.listTopics().namesToListings().get(),
-                    n -> n.containsKey(topic));
-
-            var message = "hello world";
-            producer.send(new ProducerRecord<>(topic, message)).get(5, TimeUnit.SECONDS);
+            var message = HELLO_WORLD;
+            producer.send(new ProducerRecord<>(topic.name(), message)).get(5, TimeUnit.SECONDS);
 
-            var tps = List.of(new TopicPartition(topic, 0));
+            var tps = List.of(new TopicPartition(topic.name(), 0));
             directConsumer.assign(tps);
             directConsumer.seekToBeginning(tps);
             var records = directConsumer.poll(Duration.ofSeconds(2));
@@ -166,80 +128,58 @@ class EnvelopeEncryptionFilterIT {
                     .extracting(ConsumerRecord::value)
                     .isNotEqualTo(message);
         }
-
-        IntegrationTestingKmsService.delete(kmsId.toString());
     }
 
-    @Test
-    void unencryptedRecordsConsumable(KafkaCluster cluster, KafkaProducer<String, String> directProducer) throws Exception {
-        var kmsId = UUID.randomUUID();
-        var kms = provisionKms(kmsId);
-        generateKekWithAlias(kms);
+    @TestTemplate
+    void unencryptedRecordsConsumable(KafkaCluster cluster, KafkaProducer<String, String> directProducer, Topic topic, TestKmsFacade<?, ?, ?> testKmsFacade)
+            throws Exception {
+        var testKekManager = testKmsFacade.getTestKekManager();
+        testKekManager.generateKek(topic.name());
 
         var builder = proxy(cluster);
-
-        builder.addToFilters(encryptionFilterDefinition(kmsId));
+        builder.addToFilters(buildEncryptionFilterDefinition(testKmsFacade));
 
         try (var tester = kroxyliciousTester(builder);
-                var admin = tester.admin();
                 var producer = tester.producer();
                 var consumer = tester.consumer()) {
 
-            String topic = tester.createTopic(DEFAULT_VIRTUAL_CLUSTER);
-
-            await().atMost(Duration.ofSeconds(5)).until(() -> admin.listTopics().namesToListings().get(),
-                    n -> n.containsKey(topic));
-
             // messages produced via Kroxylicious will be encrypted
-            var message = "hello encrypted world";
-            producer.send(new ProducerRecord<>(topic, message)).get(5, TimeUnit.SECONDS);
+            producer.send(new ProducerRecord<>(topic.name(), HELLO_SECRET)).get(5, TimeUnit.SECONDS);
 
             // messages produced direct will be plain
-            var plainMessage = "hello plain world";
-            directProducer.send(new ProducerRecord<>(topic, plainMessage)).get(5, TimeUnit.SECONDS);
+            directProducer.send(new ProducerRecord<>(topic.name(), HELLO_WORLD)).get(5, TimeUnit.SECONDS);
 
-            consumer.subscribe(List.of(topic));
+            consumer.subscribe(List.of(topic.name()));
             var records = consumer.poll(Duration.ofSeconds(2));
             assertThat(records.iterator()).toIterable()
                     .hasSize(2)
                     .map(ConsumerRecord::value)
-                    .containsExactly(message, plainMessage);
+                    .contains(HELLO_SECRET, HELLO_WORLD);
         }
-
-        IntegrationTestingKmsService.delete(kmsId.toString());
     }
 
-    @Test
+    @TestTemplate
     void nullValueRecordProducedAndConsumedSuccessfully(KafkaCluster cluster,
-                                                        @ClientConfig(name = ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, value = "earliest") @ClientConfig(name = ConsumerConfig.GROUP_ID_CONFIG, value = "test") Consumer<String, String> directConsumer)
+                                                        @ClientConfig(name = ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, value = "earliest") @ClientConfig(name = ConsumerConfig.GROUP_ID_CONFIG, value = "test") Consumer<String, String> directConsumer,
+                                                        Topic topic, TestKmsFacade<?, ?, ?> testKmsFacade)
             throws Exception {
-        var kmsId = UUID.randomUUID();
-        var kms = provisionKms(kmsId);
-        generateKekWithAlias(kms);
+        var testKekManager = testKmsFacade.getTestKekManager();
+        testKekManager.generateKek(topic.name());
 
         var builder = proxy(cluster);
-
-        builder.addToFilters(encryptionFilterDefinition(kmsId));
+        builder.addToFilters(buildEncryptionFilterDefinition(testKmsFacade));
 
         try (var tester = kroxyliciousTester(builder);
-                var admin = tester.admin();
                 var producer = tester.producer();
                 var consumer = tester.consumer()) {
 
-            String topic = tester.createTopic(DEFAULT_VIRTUAL_CLUSTER);
-
-            await().atMost(Duration.ofSeconds(5)).until(() -> admin.listTopics().namesToListings().get(),
-                    n -> n.containsKey(topic));
-
             String message = null;
-            producer.send(new ProducerRecord<>(topic, message)).get(5, TimeUnit.SECONDS);
+            producer.send(new ProducerRecord<>(topic.name(), message)).get(5, TimeUnit.SECONDS);
 
-            assertOnlyValueInTopicHasNullValue(consumer, topic);
+            assertOnlyValueInTopicHasNullValue(consumer, topic.name());
             // test that the null-value is preserved in Kafka to keep compaction tombstoning working
-            assertOnlyValueInTopicHasNullValue(directConsumer, topic);
+            assertOnlyValueInTopicHasNullValue(directConsumer, topic.name());
         }
-
-        IntegrationTestingKmsService.delete(kmsId.toString());
     }
 
     private static void assertOnlyValueInTopicHasNullValue(Consumer<String, String> consumer, String topic) {
@@ -252,84 +192,63 @@ class EnvelopeEncryptionFilterIT {
                 .isNull();
     }
 
-    @Test
-    void produceAndConsumeEncryptedAndPlainTopicsAtSameTime(KafkaCluster cluster, Admin admin) throws Exception {
-        var kmsId = UUID.randomUUID();
-        var kms = provisionKms(kmsId);
-        var keyId = generateKekWithAlias(kms);
-
-        var encryptedTopic = UUID.randomUUID().toString();
-        var plainTopic = UUID.randomUUID().toString();
-
-        var topics = Stream.of(encryptedTopic, plainTopic)
-                .map(t -> new NewTopic(t, Optional.empty(), Optional.empty()))
-                .toList();
-        admin.createTopics(topics).all().get(5, TimeUnit.SECONDS);
-        await().atMost(Duration.ofSeconds(5)).until(() -> admin.listTopics().namesToListings().get(),
-                n -> n.containsKey(encryptedTopic) && n.containsKey(plainTopic));
+    @TestTemplate
+    void produceAndConsumeEncryptedAndPlainTopicsAtSameTime(KafkaCluster cluster, Topic encryptedTopic, Topic plainTopic, TestKmsFacade<?, ?, ?> testKmsFacade)
+            throws Exception {
+        var testKekManager = testKmsFacade.getTestKekManager();
+        testKekManager.generateKek(encryptedTopic.name());
 
         var builder = proxy(cluster);
-
-        builder.addToFilters(encryptionFilterDefinition(kmsId));
+        builder.addToFilters(buildEncryptionFilterDefinition(testKmsFacade));
 
         try (var tester = kroxyliciousTester(builder);
                 var producer = tester.producer(Map.of(ProducerConfig.LINGER_MS_CONFIG, 1000, ProducerConfig.BATCH_SIZE_CONFIG, 2));
                 var consumer = tester.consumer()) {
 
-            var secretMessage = "hello secret";
-            var plainMessage = "hello world";
-
-            producer.send(new ProducerRecord<>(encryptedTopic, secretMessage));
-            producer.send(new ProducerRecord<>(plainTopic, plainMessage));
+            producer.send(new ProducerRecord<>(encryptedTopic.name(), HELLO_SECRET));
+            producer.send(new ProducerRecord<>(plainTopic.name(), HELLO_WORLD));
             producer.flush();
 
-            consumer.subscribe(List.of(encryptedTopic, plainTopic));
+            consumer.subscribe(List.of(encryptedTopic.name(), plainTopic.name()));
             var records = consumer.poll(Duration.ofSeconds(2));
             assertThat(records.iterator())
                     .toIterable()
                     .extracting(ConsumerRecord::value)
-                    .contains(secretMessage, plainMessage);
+                    .contains(HELLO_SECRET, HELLO_WORLD);
         }
-
-        IntegrationTestingKmsService.delete(kmsId.toString());
     }
 
-    @Test
-    void shouldGenerateOneDek(KafkaCluster cluster) throws Exception {
-        var kmsId = UUID.randomUUID();
-        var kms = provisionKms(kmsId);
-        var keyId = generateKekWithAlias(kms);
+    // TODO express this test as a unit test and consider doing away with the test as the IT level.
+    @TestTemplate
+    void shouldGenerateOneDek(KafkaCluster cluster, Topic topic, TestKmsFacade<?, ?, ?> testKmsFacade) throws Exception {
+        assumeThatCode(testKmsFacade::getKms).doesNotThrowAnyException();
+        assertThat(testKmsFacade.getKms()).isInstanceOf(InMemoryKms.class);
 
-        var builder = proxy(cluster);
+        var testKekManager = testKmsFacade.getTestKekManager();
+        testKekManager.generateKek(topic.name());
 
-        builder.addToFilters(encryptionFilterDefinition(kmsId));
+        var builder = proxy(cluster);
+        builder.addToFilters(buildEncryptionFilterDefinition(testKmsFacade));
 
         try (var tester = kroxyliciousTester(builder);
-                var admin = tester.admin();
                 var producer = tester.producer(Map.of(ProducerConfig.LINGER_MS_CONFIG, 0));
                 var consumer = tester.consumer()) {
 
-            String topic = tester.createTopic(DEFAULT_VIRTUAL_CLUSTER);
+            producer.send(new ProducerRecord<>(topic.name(), HELLO_WORLD)).get(5, TimeUnit.SECONDS);
 
-            await().atMost(Duration.ofSeconds(5)).until(() -> admin.listTopics().namesToListings().get(),
-                    n -> n.containsKey(topic));
-
-            var message = "hello world";
-            producer.send(new ProducerRecord<>(topic, message)).get(5, TimeUnit.SECONDS);
-
-            consumer.subscribe(List.of(topic));
+            consumer.subscribe(List.of(topic.name()));
             var records = consumer.poll(Duration.ofSeconds(2));
             assertThat(records.iterator())
                     .toIterable()
                     .singleElement()
                     .extracting(ConsumerRecord::value)
-                    .isEqualTo(message);
+                    .isEqualTo(HELLO_WORLD);
 
             // Send two batches to the same topic
-            message = "hello world #2";
-            producer.send(new ProducerRecord<>(topic, message)).get(5, TimeUnit.SECONDS);
+            var message = "hello world #2";
+            producer.send(new ProducerRecord<>(topic.name(), message)).get(5, TimeUnit.SECONDS);
 
-            consumer.subscribe(List.of(topic));
+            consumer.subscribe(List.of(topic.name()));
             records = consumer.poll(Duration.ofSeconds(2));
             assertThat(records.iterator())
                     .toIterable()
@@ -338,28 +257,19 @@ class EnvelopeEncryptionFilterIT {
                     .isEqualTo(message);
         }
 
-        // We expect that only one DEK was generated
-        assertThat(kms.numDeksGenerated()).isEqualTo(1);
+        assertThat(testKmsFacade.getKms())
+                .asInstanceOf(InstanceOfAssertFactories.type(InMemoryKms.class))
+                .extracting(InMemoryKms::numDeksGenerated).isEqualTo(1);
 
-        IntegrationTestingKmsService.delete(kmsId.toString());
     }
 
-    /*
-     *
-     * further IT ideas:
-     * fetching from > 1 topics (mixed encryption/plain case)
-     * exploratory test examining what the client will see/do when decryption fails - looking to verify
-     * - behaviour is reasonable
-     * - the user has a chance to understand what's wrong.
-     * behaviour when KMS throws a KmsException a) during produce, b) during fetch
-     */
-    private FilterDefinition encryptionFilterDefinition(UUID kmsId) {
-        return new FilterDefinitionBuilder("EnvelopeEncryption")
-                .withConfig("kms", IntegrationTestingKmsService.class.getSimpleName())
-                .withConfig("kmsConfig", Map.of("name", kmsId.toString()))
+    private FilterDefinition buildEncryptionFilterDefinition(TestKmsFacade<?, ?, ?> testKmsFacade) {
+        return new FilterDefinitionBuilder(EnvelopeEncryption.class.getSimpleName())
+                .withConfig("kms", testKmsFacade.getKmsServiceClass().getSimpleName())
+                .withConfig("kmsConfig", testKmsFacade.getKmsServiceConfig())
                 .withConfig("selector", TemplateKekSelector.class.getSimpleName())
-                .withConfig("selectorConfig", Map.of("template", "all"))
+                .withConfig("selectorConfig", Map.of("template", TEMPLATE_KEK_SELECTOR_PATTERN))
                 .build();
-
     }
+
 }
diff --git a/kroxylicious-integration-tests/src/test/java/io/kroxylicious/proxy/encryption/EnvelopeEncryptionTestInvocationContextProvider.java b/kroxylicious-integration-tests/src/test/java/io/kroxylicious/proxy/encryption/EnvelopeEncryptionTestInvocationContextProvider.java
new file mode 100644
index 00000000..4c92d6ef
--- /dev/null
+++ b/kroxylicious-integration-tests/src/test/java/io/kroxylicious/proxy/encryption/EnvelopeEncryptionTestInvocationContextProvider.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright Kroxylicious Authors.
+ *
+ * Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
+ */
+
+package io.kroxylicious.proxy.encryption;
+
+import java.util.List;
+import java.util.stream.Stream;
+
+import org.junit.jupiter.api.extension.AfterTestExecutionCallback;
+import org.junit.jupiter.api.extension.BeforeTestExecutionCallback;
+import org.junit.jupiter.api.extension.ConditionEvaluationResult;
+import org.junit.jupiter.api.extension.ExecutionCondition;
+import org.junit.jupiter.api.extension.Extension;
+import org.junit.jupiter.api.extension.ExtensionContext;
+import org.junit.jupiter.api.extension.ParameterContext;
+import org.junit.jupiter.api.extension.TestTemplateInvocationContext;
+import org.junit.jupiter.api.extension.TestTemplateInvocationContextProvider;
+import org.junit.jupiter.api.extension.support.TypeBasedParameterResolver;
+
+import io.kroxylicious.kms.service.TestKmsFacade;
+import io.kroxylicious.kms.service.TestKmsFacadeFactory;
+
+class EnvelopeEncryptionTestInvocationContextProvider implements TestTemplateInvocationContextProvider {
+
+    @Override
+    public boolean supportsTestTemplate(ExtensionContext context) {
+        return true;
+    }
+
+    @Override
+    public Stream<TestTemplateInvocationContext> provideTestTemplateInvocationContexts(ExtensionContext context) {
+        return TestKmsFacadeFactory.getTestKmsFacadeFactories()
+                .map(TestKmsFacadeFactory::build)
+                .map(TemplateInvocationContext::new);
+    }
+
+    private record TemplateInvocationContext(TestKmsFacade<?, ?, ?> kmsFacade) implements TestTemplateInvocationContext {
+
+        @Override
+        public String getDisplayName(int invocationIndex) {
+            return kmsFacade.getClass().getSimpleName();
+        }
+
+        @Override
+        public List<Extension> getAdditionalExtensions() {
+            if (!kmsFacade.isAvailable()) {
+                return List.of(
+                        (ExecutionCondition) extensionContext -> kmsFacade.isAvailable() ? ConditionEvaluationResult.enabled(null)
+                                : ConditionEvaluationResult.disabled(null));
+            }
+
+            return List.of(
+                    (BeforeTestExecutionCallback) extensionContext -> kmsFacade.start(),
+                    new TypeBasedParameterResolver<TestKmsFacade<?, ?, ?>>() {
+                        @Override
+                        public TestKmsFacade<?, ?, ?> resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) {
+                            return kmsFacade;
+                        }
+                    },
+                    (AfterTestExecutionCallback) extensionContext -> kmsFacade.stop());
+        }
+    }
+}
diff --git a/kroxylicious-kms-provider-hashicorp-vault-test-support/pom.xml b/kroxylicious-kms-provider-hashicorp-vault-test-support/pom.xml
new file mode 100644
index 00000000..204e2cdf
--- /dev/null
+++ b/kroxylicious-kms-provider-hashicorp-vault-test-support/pom.xml
@@ -0,0 +1,55 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+    Copyright Kroxylicious Authors.
+
+    Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
+
+-->
+
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>io.kroxylicious</groupId>
+        <artifactId>kroxylicious-parent</artifactId>
+        <version>0.4.0-SNAPSHOT</version>
+    </parent>
+
+    <artifactId>kroxylicious-kms-provider-hashicorp-vault-test-support</artifactId>
+    <description>Test support code for modules testing the HashiCorp Vault KMS</description>
+
+    <dependencies>
+        <!-- project dependencies - runtime and compile -->
+        <dependency>
+            <groupId>io.kroxylicious</groupId>
+            <artifactId>kroxylicious-kms-provider-hashicorp-vault</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>io.kroxylicious</groupId>
+            <artifactId>kroxylicious-kms-test-support</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>io.kroxylicious</groupId>
+            <artifactId>kroxylicious-runtime</artifactId>
+        </dependency>
+
+        <!-- third party dependencies - runtime and compile -->
+        <dependency>
+            <groupId>org.testcontainers</groupId>
+            <artifactId>vault</artifactId>
+        </dependency>
+        <!-- third party dependencies - test -->
+        <dependency>
+            <groupId>org.assertj</groupId>
+            <artifactId>assertj-core</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.junit.jupiter</groupId>
+            <artifactId>junit-jupiter-api</artifactId>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+</project>
\ No newline at end of file
diff --git a/kroxylicious-kms-provider-hashicorp-vault-test-support/src/main/java/io/kroxylicious/kms/provider/hashicorp/vault/VaultTestKmsFacade.java b/kroxylicious-kms-provider-hashicorp-vault-test-support/src/main/java/io/kroxylicious/kms/provider/hashicorp/vault/VaultTestKmsFacade.java
new file mode 100644
index 00000000..a7a21a09
--- /dev/null
+++ b/kroxylicious-kms-provider-hashicorp-vault-test-support/src/main/java/io/kroxylicious/kms/provider/hashicorp/vault/VaultTestKmsFacade.java
@@ -0,0 +1,169 @@
+/*
+ * Copyright Kroxylicious Authors.
+ *
+ * Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
+ */
+
+package io.kroxylicious.kms.provider.hashicorp.vault;
+
+import java.io.IOException;
+import java.net.URI;
+import java.util.Arrays;
+import java.util.Objects;
+
+import org.testcontainers.DockerClientFactory;
+import org.testcontainers.vault.VaultContainer;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+import io.kroxylicious.kms.provider.hashicorp.vault.VaultKmsService.Config;
+import io.kroxylicious.kms.provider.hashicorp.vault.VaultResponse.ReadKeyData;
+import io.kroxylicious.kms.service.KmsException;
+import io.kroxylicious.kms.service.TestKekManager;
+import io.kroxylicious.kms.service.TestKmsFacade;
+import io.kroxylicious.kms.service.UnknownAliasException;
+
+public class VaultTestKmsFacade implements TestKmsFacade<Config, String, VaultEdek> {
+    private static final String VAULT_TOKEN = "rootToken";
+    private static final String HASHICORP_VAULT = "hashicorp/vault:1.15";
+    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
+    private static final String VAULT_CMD = "vault";
+
+    @SuppressWarnings("rawtypes")
+    private VaultContainer vaultContainer;
+
+    @Override
+    public boolean isAvailable() {
+        return DockerClientFactory.instance().isDockerAvailable();
+    }
+
+    @Override
+    @SuppressWarnings("resource")
+    public void start() {
+
+        vaultContainer = new VaultContainer<>(HASHICORP_VAULT)
+                .withVaultToken(VAULT_TOKEN)
+                .withEnv("VAULT_FORMAT", "json")
+                .withInitCommand(
+                        "secrets enable transit");
+        vaultContainer.start();
+    }
+
+    @Override
+    public void stop() {
+        if (vaultContainer != null) {
+            vaultContainer.close();
+        }
+    }
+
+    @Override
+    public TestKekManager getTestKekManager() {
+        return new VaultTestKekManager();
+    }
+
+    @Override
+    public Class<VaultKmsService> getKmsServiceClass() {
+        return VaultKmsService.class;
+    }
+
+    @Override
+    public Config getKmsServiceConfig() {
+        return new Config(URI.create(vaultContainer.getHttpHostAddress()), VAULT_TOKEN);
+    }
+
+    private class VaultTestKekManager implements TestKekManager {
+        @Override
+        public void generateKek(String alias) {
+            Objects.requireNonNull(alias);
+
+            if (exists(alias)) {
+                throw new AlreadyExistsException(alias);
+            }
+            else {
+                create(alias);
+            }
+        }
+
+        @Override
+        public void rotateKek(String alias) {
+            Objects.requireNonNull(alias);
+
+            if (exists(alias)) {
+                rotate(alias);
+            }
+            else {
+                throw new UnknownAliasException(alias);
+            }
+        }
+
+        @Override
+        public boolean exists(String alias) {
+            try {
+                read(alias);
+                return true;
+            }
+            catch (RuntimeException e) {
+                if (isNoValueFound(e)) {
+                    return false;
+                }
+                else {
+                    throw e;
+                }
+            }
+        }
+
+        private boolean isNoValueFound(Exception e) {
+            return e.getMessage().contains("No value found");
+        }
+
+        private ReadKeyData create(String keyId) {
+            return runVaultCommand(new TypeReference<>() {
+            }, VAULT_CMD, "write", "-f", "transit/keys/%s".formatted(keyId));
+        }
+
+        private ReadKeyData read(String keyId) {
+            return runVaultCommand(new TypeReference<>() {
+            }, VAULT_CMD, "read", "transit/keys/%s".formatted(keyId));
+        }
+
+        private ReadKeyData rotate(String keyId) {
+            return runVaultCommand(new TypeReference<>() {
+            }, VAULT_CMD, "write", "-f", "transit/keys/%s/rotate".formatted(keyId));
+        }
+
+        private <D> D runVaultCommand(TypeReference<VaultResponse<D>> valueTypeRef, String... args) {
+            try {
+                var execResult = vaultContainer.execInContainer(args);
+                int exitCode = execResult.getExitCode();
+                if (exitCode != 0) {
+                    throw new VaultException(
+                            "Failed to run vault command: %s, exit code: %d, stderr: %s".formatted(Arrays.stream(args).toList(), exitCode, execResult.getStderr()));
+                }
+                var response = OBJECT_MAPPER.readValue(execResult.getStdout(), valueTypeRef);
+                return response.data();
+            }
+            catch (IOException e) {
+                throw new VaultException("Failed to run vault command: %s".formatted(Arrays.stream(args).toList()), e);
+            }
+            catch (InterruptedException e) {
+                Thread.currentThread().interrupt();
+                throw new VaultException(e);
+            }
+        }
+
+        private static class VaultException extends KmsException {
+            VaultException(String message) {
+                super(message);
+            }
+
+            VaultException(String message, Throwable cause) {
+                super(message, cause);
+            }
+
+            VaultException(Throwable cause) {
+                super(cause);
+            }
+        }
+    }
+}
diff --git a/kroxylicious-kms-provider-hashicorp-vault-test-support/src/main/java/io/kroxylicious/kms/provider/hashicorp/vault/VaultTestKmsFacadeFactory.java b/kroxylicious-kms-provider-hashicorp-vault-test-support/src/main/java/io/kroxylicious/kms/provider/hashicorp/vault/VaultTestKmsFacadeFactory.java
new file mode 100644
index 00000000..abf48acd
--- /dev/null
+++ b/kroxylicious-kms-provider-hashicorp-vault-test-support/src/main/java/io/kroxylicious/kms/provider/hashicorp/vault/VaultTestKmsFacadeFactory.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright Kroxylicious Authors.
+ *
+ * Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
+ */
+
+package io.kroxylicious.kms.provider.hashicorp.vault;
+
+import io.kroxylicious.kms.provider.hashicorp.vault.VaultKmsService.Config;
+import io.kroxylicious.kms.service.TestKmsFacadeFactory;
+
+/**
+ * Factory for {@link VaultTestKmsFacade}s.
+ */
+public class VaultTestKmsFacadeFactory implements TestKmsFacadeFactory<Config, String, VaultEdek> {
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public VaultTestKmsFacade build() {
+        return new VaultTestKmsFacade();
+    }
+}
diff --git a/kroxylicious-kms-provider-hashicorp-vault-test-support/src/main/resources/META-INF/services/io.kroxylicious.kms.service.TestKmsFacadeFactory b/kroxylicious-kms-provider-hashicorp-vault-test-support/src/main/resources/META-INF/services/io.kroxylicious.kms.service.TestKmsFacadeFactory
new file mode 100644
index 00000000..ffcaddb2
--- /dev/null
+++ b/kroxylicious-kms-provider-hashicorp-vault-test-support/src/main/resources/META-INF/services/io.kroxylicious.kms.service.TestKmsFacadeFactory
@@ -0,0 +1,7 @@
+#
+# Copyright Kroxylicious Authors.
+#
+# Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
+#
+
+io.kroxylicious.kms.provider.hashicorp.vault.VaultTestKmsFacadeFactory
\ No newline at end of file
diff --git a/kroxylicious-kms-provider-hashicorp-vault-test-support/src/test/java/io/kroxylicious/kms/provider/hashicorp/vault/VaultTestKmsFacadeTest.java b/kroxylicious-kms-provider-hashicorp-vault-test-support/src/test/java/io/kroxylicious/kms/provider/hashicorp/vault/VaultTestKmsFacadeTest.java
new file mode 100644
index 00000000..503ee929
--- /dev/null
+++ b/kroxylicious-kms-provider-hashicorp-vault-test-support/src/test/java/io/kroxylicious/kms/provider/hashicorp/vault/VaultTestKmsFacadeTest.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright Kroxylicious Authors.
+ *
+ * Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
+ */
+
+package io.kroxylicious.kms.provider.hashicorp.vault;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.testcontainers.DockerClientFactory;
+
+import io.kroxylicious.kms.provider.hashicorp.vault.VaultKmsService.Config;
+import io.kroxylicious.kms.service.AbstractTestKmsFacadeTest;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assumptions.assumeThat;
+
+class VaultTestKmsFacadeTest extends AbstractTestKmsFacadeTest<Config, String, VaultEdek> {
+
+    VaultTestKmsFacadeTest() {
+        super(new VaultTestKmsFacadeFactory());
+    }
+
+    @BeforeEach
+    void beforeEach() {
+        assumeThat(DockerClientFactory.instance().isDockerAvailable()).withFailMessage("docker unavailable").isTrue();
+    }
+
+    @Test
+    void classAndConfig() {
+        try (var facade = factory.build()) {
+            facade.start();
+            assertThat(facade.getKmsServiceClass()).isEqualTo(VaultKmsService.class);
+            assertThat(facade.getKmsServiceConfig()).isInstanceOf(Config.class);
+        }
+    }
+
+}
diff --git a/kroxylicious-kms-provider-hashicorp-vault/src/test/java/io/kroxylicious/kms/provider/hashicorp/vault/VaultKmsIT.java b/kroxylicious-kms-provider-hashicorp-vault/src/test/java/io/kroxylicious/kms/provider/hashicorp/vault/VaultKmsIT.java
index 95f4532c..b5f0bd2d 100644
--- a/kroxylicious-kms-provider-hashicorp-vault/src/test/java/io/kroxylicious/kms/provider/hashicorp/vault/VaultKmsIT.java
+++ b/kroxylicious-kms-provider-hashicorp-vault/src/test/java/io/kroxylicious/kms/provider/hashicorp/vault/VaultKmsIT.java
@@ -25,6 +25,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
 import io.kroxylicious.kms.provider.hashicorp.vault.VaultKmsService.Config;
 import io.kroxylicious.kms.provider.hashicorp.vault.VaultResponse.ReadKeyData;
 import io.kroxylicious.kms.service.DekPair;
+import io.kroxylicious.kms.service.UnknownAliasException;
 import io.kroxylicious.kms.service.UnknownKeyException;
 
 import static org.assertj.core.api.Assertions.assertThat;
@@ -87,7 +88,7 @@ class VaultKmsIT {
         assertThat(resolved)
                 .failsWithin(Duration.ofSeconds(5))
                 .withThrowableThat()
-                .withCauseInstanceOf(UnknownKeyException.class);
+                .withCauseInstanceOf(UnknownAliasException.class);
     }
 
     @Test
diff --git a/kroxylicious-kms-provider-kroxylicious-inmemory-test-support/pom.xml b/kroxylicious-kms-provider-kroxylicious-inmemory-test-support/pom.xml
new file mode 100644
index 00000000..fc4b9d85
--- /dev/null
+++ b/kroxylicious-kms-provider-kroxylicious-inmemory-test-support/pom.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+    Copyright Kroxylicious Authors.
+
+    Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
+
+-->
+
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>io.kroxylicious</groupId>
+        <artifactId>kroxylicious-parent</artifactId>
+        <version>0.4.0-SNAPSHOT</version>
+    </parent>
+
+    <artifactId>kroxylicious-kms-provider-kroxylicious-inmemory-test-support</artifactId>
+    <description>Test support code for modules testing the InMemory KMS</description>
+
+    <dependencies>
+        <!-- project dependencies - runtime and compile -->
+        <dependency>
+            <groupId>io.kroxylicious</groupId>
+            <artifactId>kroxylicious-kms-provider-kroxylicious-inmemory</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>io.kroxylicious</groupId>
+            <artifactId>kroxylicious-kms-test-support</artifactId>
+        </dependency>
+        <!-- third party dependencies - test -->
+        <dependency>
+            <groupId>org.assertj</groupId>
+            <artifactId>assertj-core</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.junit.jupiter</groupId>
+            <artifactId>junit-jupiter-api</artifactId>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+</project>
\ No newline at end of file
diff --git a/kroxylicious-kms-provider-kroxylicious-inmemory-test-support/src/main/java/io/kroxylicious/kms/provider/kroxylicious/inmemory/InMemoryTestKmsFacade.java b/kroxylicious-kms-provider-kroxylicious-inmemory-test-support/src/main/java/io/kroxylicious/kms/provider/kroxylicious/inmemory/InMemoryTestKmsFacade.java
new file mode 100644
index 00000000..5b614610
--- /dev/null
+++ b/kroxylicious-kms-provider-kroxylicious-inmemory-test-support/src/main/java/io/kroxylicious/kms/provider/kroxylicious/inmemory/InMemoryTestKmsFacade.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright Kroxylicious Authors.
+ *
+ * Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
+ */
+
+package io.kroxylicious.kms.provider.kroxylicious.inmemory;
+
+import java.util.Objects;
+import java.util.UUID;
+import java.util.concurrent.CompletionException;
+
+import io.kroxylicious.kms.provider.kroxylicious.inmemory.IntegrationTestingKmsService.Config;
+import io.kroxylicious.kms.service.TestKekManager;
+import io.kroxylicious.kms.service.TestKmsFacade;
+import io.kroxylicious.kms.service.UnknownAliasException;
+
+public class InMemoryTestKmsFacade implements TestKmsFacade<Config, UUID, InMemoryEdek> {
+
+    private final UUID kmsId = UUID.randomUUID();
+    private InMemoryKms kms;
+
+    @Override
+    public void start() {
+        kms = IntegrationTestingKmsService.newInstance().buildKms(new Config(kmsId.toString()));
+    }
+
+    @Override
+    public void stop() {
+        IntegrationTestingKmsService.delete(kmsId.toString());
+    }
+
+    @Override
+    public TestKekManager getTestKekManager() {
+        return new InMemoryTestKekManager();
+    }
+
+    @Override
+    public Class<IntegrationTestingKmsService> getKmsServiceClass() {
+        return IntegrationTestingKmsService.class;
+    }
+
+    @Override
+    public InMemoryKms getKms() {
+        return kms;
+    }
+
+    @Override
+    public Config getKmsServiceConfig() {
+        return new Config(kmsId.toString());
+    }
+
+    private class InMemoryTestKekManager implements TestKekManager {
+        @Override
+        public void generateKek(String alias) {
+            Objects.requireNonNull(alias);
+
+            try {
+                kms.resolveAlias(alias).toCompletableFuture().join();
+                throw new AlreadyExistsException(alias);
+            }
+            catch (CompletionException e) {
+                if (e.getCause() instanceof UnknownAliasException) {
+                    var kekId = kms.generateKey();
+                    kms.createAlias(kekId, alias);
+                }
+                else {
+                    throw e.getCause() instanceof RuntimeException re ? re : new RuntimeException(e.getCause());
+                }
+            }
+        }
+
+        @Override
+        public void rotateKek(String alias) {
+            Objects.requireNonNull(alias);
+
+            try {
+                kms.resolveAlias(alias).toCompletableFuture().join();
+                var kekId = kms.generateKey();
+                kms.createAlias(kekId, alias);
+            }
+            catch (CompletionException e) {
+                throw e.getCause() instanceof RuntimeException re ? re : new RuntimeException(e.getCause());
+            }
+        }
+
+        @Override
+        public boolean exists(String alias) {
+            try {
+                kms.resolveAlias(alias).toCompletableFuture().join();
+                return true;
+            }
+            catch (CompletionException e) {
+                if (e.getCause() instanceof UnknownAliasException) {
+                    return false;
+                }
+                throw e.getCause() instanceof RuntimeException re ? re : new RuntimeException(e.getCause());
+            }
+        }
+    }
+}
diff --git a/kroxylicious-kms-provider-kroxylicious-inmemory-test-support/src/main/java/io/kroxylicious/kms/provider/kroxylicious/inmemory/InMemoryTestKmsFacadeFactory.java b/kroxylicious-kms-provider-kroxylicious-inmemory-test-support/src/main/java/io/kroxylicious/kms/provider/kroxylicious/inmemory/InMemoryTestKmsFacadeFactory.java
new file mode 100644
index 00000000..4161493f
--- /dev/null
+++ b/kroxylicious-kms-provider-kroxylicious-inmemory-test-support/src/main/java/io/kroxylicious/kms/provider/kroxylicious/inmemory/InMemoryTestKmsFacadeFactory.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright Kroxylicious Authors.
+ *
+ * Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
+ */
+
+package io.kroxylicious.kms.provider.kroxylicious.inmemory;
+
+import java.util.UUID;
+
+import io.kroxylicious.kms.provider.kroxylicious.inmemory.IntegrationTestingKmsService.Config;
+import io.kroxylicious.kms.service.TestKmsFacadeFactory;
+
+/**
+ * Factory for {@link InMemoryTestKmsFacade}s.
+ */
+public class InMemoryTestKmsFacadeFactory implements TestKmsFacadeFactory<Config, UUID, InMemoryEdek> {
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public InMemoryTestKmsFacade build() {
+        return new InMemoryTestKmsFacade();
+    }
+}
diff --git a/kroxylicious-kms-provider-kroxylicious-inmemory-test-support/src/main/resources/META-INF/services/io.kroxylicious.kms.service.TestKmsFacadeFactory b/kroxylicious-kms-provider-kroxylicious-inmemory-test-support/src/main/resources/META-INF/services/io.kroxylicious.kms.service.TestKmsFacadeFactory
new file mode 100644
index 00000000..702ca3da
--- /dev/null
+++ b/kroxylicious-kms-provider-kroxylicious-inmemory-test-support/src/main/resources/META-INF/services/io.kroxylicious.kms.service.TestKmsFacadeFactory
@@ -0,0 +1,7 @@
+#
+# Copyright Kroxylicious Authors.
+#
+# Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
+#
+
+io.kroxylicious.kms.provider.kroxylicious.inmemory.InMemoryTestKmsFacadeFactory
diff --git a/kroxylicious-kms-provider-kroxylicious-inmemory-test-support/src/test/java/io/kroxylicious/kms/provider/kroxylicious/inmemory/InMemoryTestKmsFacadeTest.java b/kroxylicious-kms-provider-kroxylicious-inmemory-test-support/src/test/java/io/kroxylicious/kms/provider/kroxylicious/inmemory/InMemoryTestKmsFacadeTest.java
new file mode 100644
index 00000000..04fc87a7
--- /dev/null
+++ b/kroxylicious-kms-provider-kroxylicious-inmemory-test-support/src/test/java/io/kroxylicious/kms/provider/kroxylicious/inmemory/InMemoryTestKmsFacadeTest.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright Kroxylicious Authors.
+ *
+ * Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
+ */
+
+package io.kroxylicious.kms.provider.kroxylicious.inmemory;
+
+import java.util.UUID;
+
+import org.junit.jupiter.api.Test;
+
+import io.kroxylicious.kms.provider.kroxylicious.inmemory.IntegrationTestingKmsService.Config;
+import io.kroxylicious.kms.service.AbstractTestKmsFacadeTest;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class InMemoryTestKmsFacadeTest extends AbstractTestKmsFacadeTest<Config, UUID, InMemoryEdek> {
+
+    InMemoryTestKmsFacadeTest() {
+        super(new InMemoryTestKmsFacadeFactory());
+    }
+
+    @Test
+    void classAndConfig() {
+        try (var facade = factory.build()) {
+            facade.start();
+            assertThat(facade.getKmsServiceClass()).isEqualTo(IntegrationTestingKmsService.class);
+            assertThat(facade.getKmsServiceConfig()).isInstanceOf(Config.class);
+        }
+    }
+
+}
diff --git a/kroxylicious-kms-test-support/pom.xml b/kroxylicious-kms-test-support/pom.xml
new file mode 100644
index 00000000..9969764b
--- /dev/null
+++ b/kroxylicious-kms-test-support/pom.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+    Copyright Kroxylicious Authors.
+
+    Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
+
+-->
+
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>io.kroxylicious</groupId>
+        <artifactId>kroxylicious-parent</artifactId>
+        <version>0.4.0-SNAPSHOT</version>
+    </parent>
+
+    <artifactId>kroxylicious-kms-test-support</artifactId>
+    <description>Test support for modules testing KMS implementations.</description>
+
+    <dependencies>
+        <dependency>
+            <groupId>io.kroxylicious</groupId>
+            <artifactId>kroxylicious-kms</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.assertj</groupId>
+            <artifactId>assertj-core</artifactId>
+            <scope>compile</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.junit.jupiter</groupId>
+            <artifactId>junit-jupiter-api</artifactId>
+            <scope>compile</scope>
+        </dependency>
+    </dependencies>
+    <profiles>
+        <profile>
+            <id>ci</id>
+            <properties>
+                <sonar.coverage.exclusions>**/AbstractTestKmsFacadeTest.java</sonar.coverage.exclusions>
+            </properties>
+        </profile>
+    </profiles>
+
+</project>
\ No newline at end of file
diff --git a/kroxylicious-kms-test-support/src/main/java/io/kroxylicious/kms/service/AbstractTestKmsFacadeTest.java b/kroxylicious-kms-test-support/src/main/java/io/kroxylicious/kms/service/AbstractTestKmsFacadeTest.java
new file mode 100644
index 00000000..ed987ce1
--- /dev/null
+++ b/kroxylicious-kms-test-support/src/main/java/io/kroxylicious/kms/service/AbstractTestKmsFacadeTest.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright Kroxylicious Authors.
+ *
+ * Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
+ */
+
+package io.kroxylicious.kms.service;
+
+import java.util.Objects;
+
+import org.junit.jupiter.api.Test;
+
+import io.kroxylicious.kms.service.TestKekManager.AlreadyExistsException;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+/**
+ * Abstract
+ * @param <C> The config type
+ * @param <K> The key reference
+ * @param <E> The type of encrypted DEK
+ */
+@SuppressWarnings("java:S5960") // this is test code, it exists in the main module to facilitate its use by concrete test cases
+public abstract class AbstractTestKmsFacadeTest<C, K, E> {
+
+    private static final String ALIAS = "myalias";
+    protected final TestKmsFacadeFactory<C, K, E> factory;
+
+    protected AbstractTestKmsFacadeTest(TestKmsFacadeFactory<C, K, E> factory) {
+        Objects.requireNonNull(factory);
+        this.factory = factory;
+    }
+
+    @Test
+    void factory() {
+        try (var facade = factory.build()) {
+            assertThat(facade)
+                    .isNotNull()
+                    .extracting(TestKmsFacade::isAvailable)
+                    .isEqualTo(true);
+        }
+    }
+
+    @Test
+    void generateKek() {
+        try (var facade = factory.build()) {
+            facade.start();
+            var manager = facade.getTestKekManager();
+            assertThat(manager.exists(ALIAS)).isFalse();
+            manager.generateKek(ALIAS);
+            assertThat(manager.exists(ALIAS)).isTrue();
+        }
+    }
+
+    @Test
+    void generateKekFailsIfAliasExists() {
+        try (var facade = factory.build()) {
+            facade.start();
+            var manager = facade.getTestKekManager();
+            manager.generateKek(ALIAS);
+
+            assertThatThrownBy(() -> manager.generateKek(ALIAS))
+                    .isInstanceOf(AlreadyExistsException.class);
+        }
+    }
+
+    @Test
+    void rotateKek() {
+        try (var facade = factory.build()) {
+            facade.start();
+            var manager = facade.getTestKekManager();
+            manager.generateKek(ALIAS);
+            assertThat(manager.exists(ALIAS)).isTrue();
+
+            manager.rotateKek(ALIAS);
+        }
+    }
+
+    @Test
+    void rotateKekFailsIfAliasDoesNotExist() {
+        try (var facade = factory.build()) {
+            facade.start();
+            var manager = facade.getTestKekManager();
+
+            assertThatThrownBy(() -> manager.rotateKek(ALIAS))
+                    .isInstanceOf(UnknownAliasException.class);
+        }
+    }
+}
diff --git a/kroxylicious-kms-test-support/src/main/java/io/kroxylicious/kms/service/TestKekManager.java b/kroxylicious-kms-test-support/src/main/java/io/kroxylicious/kms/service/TestKekManager.java
new file mode 100644
index 00000000..497efe8c
--- /dev/null
+++ b/kroxylicious-kms-test-support/src/main/java/io/kroxylicious/kms/service/TestKekManager.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright Kroxylicious Authors.
+ *
+ * Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
+ */
+
+package io.kroxylicious.kms.service;
+
+/**
+ * Exposes the ability to manage the KEKs on a KMS implementation.
+ */
+public interface TestKekManager {
+    /**
+     * Creates a KEK in the KMS with given alias.
+     *
+     * @param alias kek alias
+     * @throws AlreadyExistsException alias already exists
+     */
+    void generateKek(String alias);
+
+    /**
+     * Rotates the kek with the given alias
+     *
+     * @param alias kek alias
+     * @throws UnknownAliasException a KEK with the given alias is not found
+     */
+    void rotateKek(String alias);
+
+    /**
+     * Tests whether kek with given alias exists.
+     *
+     * @param alias kek alias
+     * @return true if the alias exist, false otherwise.
+     */
+    boolean exists(String alias);
+
+    class AlreadyExistsException extends KmsException {
+        public AlreadyExistsException(String alias) {
+            super(alias);
+        }
+    }
+
+}
diff --git a/kroxylicious-kms-test-support/src/main/java/io/kroxylicious/kms/service/TestKmsFacade.java b/kroxylicious-kms-test-support/src/main/java/io/kroxylicious/kms/service/TestKmsFacade.java
new file mode 100644
index 00000000..48618f96
--- /dev/null
+++ b/kroxylicious-kms-test-support/src/main/java/io/kroxylicious/kms/service/TestKmsFacade.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright Kroxylicious Authors.
+ *
+ * Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
+ */
+
+package io.kroxylicious.kms.service;
+
+/**
+ * Represents the Kms itself, exposed for test purpose.
+ * @param <C> The config type
+ * @param <K> The key reference
+ * @param <E> The type of encrypted DEK
+ */
+public interface TestKmsFacade<C, K, E> extends AutoCloseable {
+
+    /**
+     * Returns true of this facade is available, or false otherwise.
+     * @return true if available, false otherwise.
+     */
+    default boolean isAvailable() {
+        return true;
+    }
+
+    /**
+     * Starts the underlying KMS.
+     */
+    void start();
+
+    /**
+     * Stops the underlying KMS.
+     */
+    void stop();
+
+    /**
+     * Gets a manager capable of managing the KEKs on the underlying KMS.
+     * @return kek manager
+     */
+    TestKekManager getTestKekManager();
+
+    /**
+     * Gets the service class of used by Kroxylicious with the underlying KMS.
+     *
+     * @return service class
+     */
+    Class<? extends KmsService<C, K, E>> getKmsServiceClass();
+
+    /**
+     * Gets the configuration Kroxylicious will need to use to connect to the underlying KMS.
+     * @return service configuration.
+     */
+    C getKmsServiceConfig();
+
+    /**
+     * Returns the actual {@link Kms} in-use by Kroxylicious.  This is an optional method.
+     * It is expected most implementations will throw an {@link UnsupportedOperationException}.
+     *
+     * @return service instance
+     * @throws UnsupportedOperationException operation is not supported.
+     */
+    default Kms<K, E> getKms() {
+        throw new UnsupportedOperationException();
+    }
+
+    default void close() {
+        stop();
+    }
+}
diff --git a/kroxylicious-kms-test-support/src/main/java/io/kroxylicious/kms/service/TestKmsFacadeFactory.java b/kroxylicious-kms-test-support/src/main/java/io/kroxylicious/kms/service/TestKmsFacadeFactory.java
new file mode 100644
index 00000000..e21cd106
--- /dev/null
+++ b/kroxylicious-kms-test-support/src/main/java/io/kroxylicious/kms/service/TestKmsFacadeFactory.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright Kroxylicious Authors.
+ *
+ * Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
+ */
+
+package io.kroxylicious.kms.service;
+
+import java.util.ServiceLoader;
+import java.util.stream.Stream;
+
+/**
+ * Factory for {@link TestKmsFacade}s.
+ * @param <C> The config type
+ * @param <K> The key reference
+ * @param <E> The type of encrypted DEK
+ */
+public interface TestKmsFacadeFactory<C, K, E> {
+
+    /**
+     * Creates a TestKmsFacade instance
+     *
+     * @return instance
+     */
+    TestKmsFacade<C, K, E> build();
+
+    /**
+     * Discovers the available {@link TestKmsFacadeFactory}.
+     * @return factories
+     */
+    @SuppressWarnings("unchecked")
+    static <C, K, E> Stream<TestKmsFacadeFactory<C, K, E>> getTestKmsFacadeFactories() {
+        return ServiceLoader.load(TestKmsFacadeFactory.class).stream()
+                .map(ServiceLoader.Provider::get);
+    }
+}