GUACAMOLE-2137 : OpenBao/Hashicorp Vault extension #1216
Conversation
|
Rebased on staging/1.6.1 and the PR updated |
…apache#1214 - It supports both OpenBao and Hashicorp Vault - Uses url like tokens of the form "vault://<mount>/<path>/<secret>" - Uses the path-help function of the vault to determine the vault type - Uses spring-vault to communicate with the vault - Adds support for both KV_1 and KV_2 Key-Value secret engines - Adds support for LDAP secret engine and static, dynamic and service accounts - Adds support of the SSH secret engine including both SSH one-time passwords and signed user certificates - Adds supports for the database secret engine, allowing Guacamole itself to obtain its username, password and if the database is configured with additional static values, the URI of the database server and the database itself - Adds the possiblity of including sub-tokens with the Vault tokens (ex: vault://kv1/users{GUAC_USERNAME}/password) - Gets the connectionGroup and User fallback functions in apache#1116 to actually work - Doesn't use a Base64 configuration string, but real Guacamole configuration options - Doesn't use a sanitize function on TextFields of the Form, but rather PasswordField types
…e statements. Remove unused GuacamoleExceptionSupplier
…introdcude in last commit
…same logic for safe use as a HashMap key
… that the Future isn't invalidated in the cache before its returned
…able or not regular files
- Use final on all local and method arguments where possible
- Comment all public methods and constructor
- Catch only specific exceptions
- Remove unused imports
- Remove code that can no longer be used
- No nested if block more than 2 levels deep
- Prefer getter functions to direct access to class variables
- Don't throw and catch exception in the same function, the the catch blocks
closer to where the exceptions are thrown
- Split HvClientProvider out from HvSecretService to reduce complexity and
increase readibility of both classes
- Create helper function for HvSecretService.prepareToken to regroup similar
logic and reduce CognitiveComplexity of prepareToken
- Split HvClient fallback logic into a seperate function, use an ordered List
of HvClients in this function to simplify the logic
|
I converted this PR to a Draft as after having run the code through a SAST/linter and taking a deep breath after the Didn't last long in Draft form, 'cause I committed my changes a few hours after following testing |
…ionGroup information can be updated for the UI
…ider class. Correct some comments
…irectoryService didn't unwrap HvConnectionGroup
|
For discussion of my proposed TokenFilter change, please see regex101.com for an example of how the regular expression I propose functions |
- Add ECDSA ec256 to Guacamole signed certifcates
- Add ability to get the Vault to issue the certificate
- Add URL query parameters to modify certificate type on a token
by token basis.
…che key, to allow tokens from different secret records on the same connection
…cret engine for consistency with the vault itself
…ce to avoid casting warning
…igned_key' and 'private_key' secret values to issued SSH certificates
…e same way as for issued certificates
|
I've been going through this branch with a view to helping it land, and built the The token pattern change alters existing behaviorI wrote characterization tests for the current parameter token grammar — they pass on Those three methods cover six distinct inputs (JUnit reports the first failing
Two distinct causes: Unrecognized modifiers. On Names outside Why strict mode matters here specifically
catch (GuacamoleTokenUndefinedException e) {
logger.debug("Secret for token \"{}\" will not be retrieved. ...");
continue;
}So for a mapping entry whose secret name contains I've opened those tests separately as #1231 (GUACAMOLE-2302) against Two things I checked that turned out fine: no catastrophic backtracking on the new
|
Thanks for this, and you're right that teh TokenFIlter is the only change to the main Guacamole code so it needs particular care so that the existing KSM module is not impacted.
However, the test suite you've built implements the existing behaviour, whereas the request here was to modify this behaviour to allow URL like token parameters. A complex example of a token parameter this Hashicorp/OpenBao extension supports is So pinning down the minimal set of changes to the existing tokenFilter is a good thing, but changes will be needed to support tokens of the above form. My approach in the filter proposed esentially let a lot of things through. If this causes problems for KSM a minimal regular expression to match url like token as above [a-z]+:\/(?:\/[a-zA-Z0-9_-]+)+(?:\?(?:[^=&?#:]+=[^&#]*(?:&[^=&?#]+=[^&#:]*)*)?)?So an alternative tokenfilter might be to replace * entire token itself.
*/
- private final Pattern tokenPattern = Pattern.compile("(.*?)(^|.)(\\$\\{([A-Za-z0-9_]*)(\\:(.*))?\\})");
+ private final Pattern tokenPattern = Pattern.compile("(.*?)(^|.)(\\$\\{([A-Za-z0-9_]*|[a-z]+:\\/(?:\\/[a-zA-Z0-9_-]+)+(?:\\?(?:[^=&?#]+=[^&#:]*(?:&[^=&?#]+=[^&#:]*)*)?)?)(\\:([^:]*))?\\})");
/**This is probably a better starting point for the regular expression if the goal is to make minimal changing to the existing behaviour
My initial TokenFilter didn't try to keep any existing behavior so this doesn't surprise me at all... Can you try with the TokenFilter propsoed above ? After that we really need to tie down exactly what behavior in the token filter will casue problems with KSM
Ok, so here is the key statement... The initial proposed changes completely modified the behavior of unmatched tokens for
I think the proposed TokenFilter above that matches more strictly url like vault tokens might address most of this concern. It can't entiremy address it as if you write a token that matches the url token filter it will match and have this behavior, but that is the desired outcome for me.
I initially dropped the one argument form as this was done in #1116.. No problem to keep both forms
Yeah I didn't quite understand the use of this function either, so I just duplicated what KSM did... If what you say above is true (I don't doubt it) then KSM also has this same issue, except ...
I only matched sub-tokens GUAC_USERNAME These allow the use of ".." or "/" in their values ? None of these values are under user control so the security risk of the path traversal attack you describe is low, but ok I'll canonicalize the token before substitution
Firstly using url like token the need to the yaml token file essentially disappears as there is no token that can't be described directly in the connections. I hestitated to entirely drop the use of the yaml file as uneeded.. However yes you have put your finger on an issue I was entirely aware of (one of the reasons I hestitated to drop the getValue code path) that getValue looks up each token value independently... The problem is imagine the yaml values LINUX_SSH_PUBLIC: vault://ssh/guacamole/server1/sign/public These two values are associated with each other and must be calculated together. So as each is looked up indenpendantly in getValue(), the cache resolution of these two values within the cache window is necessary to allow them to be associated with each other. Note that the 'engine' is coded into the vault path used in the cache and so the cache resolution doesn't allow secret resolution between two different secret engines.. I also coded the username into the cache path to avoid users usurping the secrets of another user... I don't believe I've left a possible attack path in this code, but if you still think it is an issue, best solution is just to entirely drop the yaml file support for the Hashicorp tokens as it really isn't as needed as form KSM.
The problem of code written over several weeks.. SUre I'll fix this
Frankly, I haven't looked at this code for a little while as I've moved on the other thing during the wait for review... If you want to try out the above TokenFilter with your test code that would be great |
|
Here is a regex101 link to the proposed TokenFilter for testing. Please note that the escape charceters |
This PR was first based on #1143, but has now combined with #1116 as well, so I stole the Jira ticket of #1116. The main changes are
As this PR is fully documented in the markdown/jinja I won't do in to details of the working of this extension, but rather discuss the problems I encountered
Simplified token names:
The KSM extension uses token names like KEEPER_SERVER_USERNAME as only a single Keeper secret record with relatively well controls key values is used. This extension allows basically any mount path of the secret engine and path to the secret record within the Vault and so multiple secret records. Its therefore difficult to see how the KSM token naming method could be projected on to a Vault.
As in #1116 I chose to expose the Vault paths directly in the tokens, allowing the complete freedom. This means that the file
vault-token-mapping.ymlis probably not needed,Token paths:
In most cases the token name to use will be the path to the Vault secret record post-pending with the secret value to extract. This has several issues
Secret caching:
When obtaining a secret record from the Vault, the secrets in the record are associated between themselves. For example a dynamic LDAP account has a single use username and password. The structure of the base vault extension assumes a function getValue that returns a single secret value. We therefore need to cache the values for a single record to keep the association between common values.
In the getTokens method the VaultSecretService, this is not a problem as all tokens for a single connection are resolved at once and we can set a UUID as a key in the cache to ensure common values are kept together. It also prevents session stealing by timing issues in concurrent connections. For the secrets looked up for guacamole.propertes.vlt that basically only concern secrets without any user context this is no problem. The issue is with tokens looked up in vault-token-mapping.yml. These secrets are looked up individually, but they have a user context. To avoid secret stealing and try to keep common secrets together I used a cache key combining GUAC_USERNAME, USERNAME and the path to the Vault secret record.
By the nature of Vault tokens where the password rotation at each usage, this cache should only be very short term, in the order of a few seconds. I used caffiene for the cache
Spring-Vault :
Due to the Tomcat 9 dependency of Guacamole I was forced to use a version of spring-vault compatible with Java-11, so I'm left using version 2.3.4 of spring-vault, when spring-vault is currently working on version 4.x. This came with many issues, the main one being that the LifecycleAwareSessionManager class of spring-vault in version 2.3.4 is pretty much broken, so I had to reimplement my own SessionManager. The use of spring-vault in this pull-request is therefore more of a promise to be able to simplify the extension when Guacamole migrates to a newer Tomcat
SSH ed25519:
The SSH certificate signing reqires Guacamole to be able to generate temporary SSH certificates. I used apache SSHD for this. The Java 11 versions of apache SSHD don't include ed25519 directly and I had to include i2p as a crypto provider for ed25519. I removed the BouncyCastle Fips crypto provider from #1116, as it wasn't initially used and it prevented the use of ed25519 as FIPS-140 is a relatively old standard that doesn't include or allow elliptique curve crypto
LDAP Service accounts:
The LDAP service accounts in the Vault must be checked out and checked back in again after use. This presented two problems for this extension.
Non renewable or expired token
Even in recent spring-vault implementations of LifecycleAwareSessionManager, an expired or non renewable token will cause the SessionManager to just stop in expiry. I'd like to use VaultAgent for complex authentification methods via a token sink file. This essentially means that a reauthentication via a call to the function ClientAuthentication.login() of spring-vault should be used if the token is expired or non-renewable. I implemented this in my SessionManager