Skip to content

GUACAMOLE-2196: OpenBao Vault Integration Extension#1143

Open
subbareddyalamur wants to merge 2 commits intoapache:mainfrom
subbareddyalamur:feature/openbao-vault-extension
Open

GUACAMOLE-2196: OpenBao Vault Integration Extension#1143
subbareddyalamur wants to merge 2 commits intoapache:mainfrom
subbareddyalamur:feature/openbao-vault-extension

Conversation

@subbareddyalamur
Copy link
Copy Markdown

@subbareddyalamur subbareddyalamur commented Dec 30, 2025

OpenBao Vault Extension for Apache Guacamole

This extension integrates Apache Guacamole with OpenBao vault to automatically retrieve connection credentials from OpenBao at connection time.

Overview

The OpenBao vault extension allows Guacamole to retrieve credentials from an OpenBao server using token-based authentication. Connection parameters configured with special tokens like ${OPENBAO_SECRET} are automatically replaced with values retrieved from OpenBao when a user connects.

Features

  • Automatic Credential Retrieval: Fetches credentials from OpenBao without requiring users to re-enter passwords
  • Token-Based Resolution: Uses ${OPENBAO_SECRET} and ${GUAC_USERNAME} tokens in connection parameters
  • KV v2 Support: Works with OpenBao KV v2 secrets engine
  • Simple Configuration: Minimal configuration required in guacamole.properties
  • Secure: Uses OpenBao's token-based authentication for secure API access

How It Works

  1. User logs into Guacamole with their username
  2. User initiates a connection configured with ${OPENBAO_SECRET} token
  3. Extension queries OpenBao API to retrieve the secret for that username
  4. Password is extracted and injected into the connection parameters
  5. Connection proceeds with the retrieved credentials

Configuration

OpenBao Server Setup

Before using this extension, you need:

  1. An OpenBao server running and accessible from the Guacamole server
  2. A KV v2 secrets engine mounted (eg path: guacamole-credentails)
  3. An OpenBao authentication token with read access to the secrets
  4. Secrets stored with a password field in the data

Example secret structure:

{
  "data": {
    "data": {
      "username": "user1",
      "password": "SecretPassword123"
    }
  }
}

Guacamole Configuration

Add the following properties to guacamole.properties:

# OpenBao server URL (required)
openbao-server-url: http://openbao.example.com:8200

# OpenBao authentication token (required unless AppRole is configured)
openbao-token: s.YourTokenHere

# KV mount path (optional, default: guacamole-credentails)
openbao-mount-path: guacamole-credentails

AppRole Authentication (optional)

As an alternative to a static openbao-token, the extension can
authenticate via the AppRole auth method, which typically issues
shorter-lived tokens:

# Required for AppRole
openbao-role-id:   <role-id>
openbao-secret-id: <secret-id>

# Optional; defaults to "approle"
openbao-approle-path: approle

When both openbao-role-id and openbao-secret-id are set, the
extension performs an AppRole login at
/v1/auth/<openbao-approle-path>/login and uses the returned
client_token for all subsequent requests. The token is cached and
refreshed automatically on a 403 response. If AppRole is configured,
openbao-token is ignored.

Note: The extension uses hardcoded defaults for:

  • KV version: 2 (KV v2 secrets engine)
  • Connection timeout: 5000ms (5 seconds)
  • Request timeout: 10000ms (10 seconds)

Connection Configuration

When creating connections in Guacamole, use these token patterns:

  • ${OPENBAO_SECRET}: Replaced with the password field of the
    secret stored at <mount>/data/<guac-username>.
  • ${GUAC_USERNAME}: Replaced with the logged-in Guacamole username.

Example RDP connection:

  • Username: ${GUAC_USERNAME}
  • Password: ${OPENBAO_SECRET}
  • Hostname: 192.168.1.100

Arbitrary Secret Paths (via token mapping)

For secrets that are not stored at the per-user path, an additional
name format can be used in the vault token mapping YAML
(openbao-token-mapping.yml):

openbao:<path>[:<field>]
  • <path> is the path under the configured mount, without the
    /data/ prefix (KV v2 is handled automatically).
  • <field> selects which field to return from the secret's data;
    defaults to password if omitted.

Example openbao-token-mapping.yml:

DB_PASSWORD: "openbao:db/prod-ro:password"
SSH_KEY:     "openbao:ssh-keys/${GUAC_USERNAME}:private_key"
JUMPBOX_PW:  "openbao:shared/jumpbox"

This mechanism is additive. Existing configurations that use only
${OPENBAO_SECRET} continue to work unchanged.

Secret Path Mapping

The extension maps Guacamole usernames directly to OpenBao secret paths:

Guacamole username: "john"
OpenBao secret path: /v1/guacamole-credentails/data/john

For each user, create a corresponding secret in OpenBao at the path matching their Guacamole username.

Building

Build the extension from the guacamole-client source tree:

cd extensions/guacamole-vault
mvn clean package

The built extension will be located at:

modules/guacamole-vault-openbao/target/guacamole-vault-openbao-<version>.jar

Installation

  1. Copy the built JAR to the Guacamole extensions directory:

    cp guacamole-vault-openbao-*.jar /etc/guacamole/extensions/
  2. Ensure guacamole-vault-base-*.jar is also present in the extensions directory (it's a dependency)

  3. Configure guacamole.properties as described above

  4. Restart Guacamole (e.g., restart Tomcat)

Security Considerations

  1. Protect the OpenBao Token: Use a dedicated token with minimal permissions (read-only access to required secret paths)

  2. Use TLS in Production: Always use HTTPS URLs for OpenBao in production:

    openbao-server-url: https://openbao.example.com:8200
  3. Network Security: Restrict OpenBao access to the Guacamole server using firewall rules

  4. Audit Logging: Enable OpenBao audit logging to track credential access

  5. Token Rotation: Regularly rotate OpenBao tokens and update the configuration

Troubleshooting

Extension Not Loading

Check the Guacamole logs (typically in Tomcat's catalina.out) for errors. Common issues:

  • Missing guacamole-vault-base dependency
  • Incorrect permissions on JAR files
  • Configuration errors in guacamole.properties

Secret Not Found

Error: Secret not found in OpenBao for username: john

Solutions:

  1. Verify the secret exists in OpenBao at the expected path
  2. Check that the Guacamole username matches the secret name in OpenBao
  3. Verify the token has read access to the secret

Permission Denied

Error: Permission denied accessing OpenBao. Check token permissions.

Solutions:

  1. Verify the token has appropriate policies attached
  2. Check that the token hasn't expired
  3. Ensure the token has read access to the KV mount path

Connection Timeout

Error: Failed to communicate with OpenBao

Solutions:

  1. Verify OpenBao is accessible from the Guacamole server
  2. Check firewall rules between Guacamole and OpenBao
  3. Verify the OpenBao URL is correct in the configuration

Example Deployment

  1. Setup OpenBao:

    # Start OpenBao
    bao server -dev
    
    # Enable KV v2 engine
    bao secrets enable -path=guacamole-credentails kv-v2
    
    # Create a secret
    bao kv put guacamole-credentails/john password=SecretPass123
  2. Configure Guacamole:

    openbao-server-url: http://openbao.example.com:8200
    openbao-token: s.yourtokenhere
    openbao-mount-path: guacamole-credentails
  3. Create Connection:

    • Name: Windows Server
    • Protocol: RDP
    • Hostname: 192.168.1.100
    • Username: ${GUAC_USERNAME}
    • Password: ${OPENBAO_SECRET}
  4. Connect: Log in as user "john" and connect to the Windows Server connection. The password will be automatically retrieved from OpenBao.

License

This extension is licensed under the Apache License, Version 2.0. See the LICENSE file for details.

Support

For issues or questions:

@subbareddyalamur subbareddyalamur force-pushed the feature/openbao-vault-extension branch from 24da733 to c0449db Compare December 30, 2025 09:30
@subbareddyalamur subbareddyalamur changed the title OpenBao Vault Integration Extension GUACAMOLE-2196: OpenBao Vault Integration Extension Dec 31, 2025
@necouchman
Copy link
Copy Markdown
Contributor

@subbareddyalamur Please:

@subbareddyalamur subbareddyalamur force-pushed the feature/openbao-vault-extension branch from c0449db to ecca633 Compare February 17, 2026 05:10
@subbareddyalamur
Copy link
Copy Markdown
Author

@subbareddyalamur Please:

@necouchman Done.

Copy link
Copy Markdown
Member

@corentin-soriano corentin-soriano left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for this contribution.
I've added a few questions to the code.

logger.warn("Password field not found in OpenBao response");
return null;

} catch (Exception e) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we catch a more specific exception?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pushed a new commit @corentin-soriano

} else {
logger.warn("Password not found in OpenBao for user: {}", username);
}
} catch (Exception e) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we catch a more specific exception?


logger.info("Fetching secret from OpenBao: {}", fullUrl);

try (CloseableHttpClient httpClient = HttpClients.createDefault()) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a specific reason to re-instantiate an HTTP client for each request?

Comment on lines +74 to +77
String serverUrl = configService.getServerUrl();
String token = configService.getToken();
String mountPath = configService.getMountPath();
String kvVersion = configService.getKvVersion();
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What will happen if the URL, path, or token is not defined or is empty?

Copy link
Copy Markdown

@adb014 adb014 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I started looking at doing this myself, as it was the one thing missing from Guacamole that prevented integration with our KMS and using guacamole as a low cost bastion. So it was a good thing I looked at the existing PRs before getting too far.

I'm not part of the project, so take or leave my comments as you wish... In any case I'd be very happy to see this integrated with Guacamole, so you have my upvote ;-)

Comment on lines +33 to +34
public static final StringGuacamoleProperty OPENBAO_SERVER_URL =
new StringGuacamoleProperty() {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this return a URIGuacamoleProperty

* @throws GuacamoleException
* If the property is not defined in guacamole.properties.
*/
public String getServerUrl() throws GuacamoleException {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this return a "java.net.URI"

Comment on lines +28 to +33
import org.apache.hc.client5.http.classic.methods.HttpGet;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.apache.hc.core5.http.io.entity.EntityUtils;
import org.apache.hc.core5.util.Timeout;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't it be better to use "org.springframework.vault" than writing all the code to communicate with openbao yourself ?

Copy link
Copy Markdown

@adb014 adb014 Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Something like

package org.apache.guacamole.vault.openbao.secret;

import javax.inject.Inject;
import javax.inject.Singleton;
import java.net.URI;
import java.time.Duration;
import java.util.Map;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.GuacamoleServerException;
import org.apache.guacamole.vault.openbao.conf.OpenBaoConfigurationService;
import org.springframework.vault.VaultException;
import org.springframework.vault.authentication.TokenAuthentication;
import org.springframework.vault.client.VaultEndpoint;
import org.springframework.vault.core.VaultKeyValueOperations;
import org.springframework.vault.core.VaultTemplate;
import org.springframework.vault.support.VaultResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;


public class OpenBaoClient {
    /**
     * Logger for this class.
     */
    private static final Logger logger = LoggerFactory.getLogger(OpenBaoClient.class);

    /**
     * Service for retrieving OpenBao configuration.
     */
    @Inject
    private OpenBaoConfigurationService configService;
    
    /**
     * Vault Key/Value object to retrieve values based on their key
     */
    private final VaultKeyValueOperations kvOperations;


    /**
     * KV.1 ou KV.2 template of teh mount mount
     */
    private final VaultTemplate vaultTemplate;

    /**
     * Complete the instantiantion of the class after injection of confService
     */
    @Inject
    public init() {

        VaultEndpoint endpoint = VaultEndpoint.from(configService.getServerUrl())
        );

        endpoint.setConnectionTimeout(
                Duration.ofMillis(configService.getConnectionTimeout()));
        endpoint.setReadTimeout(
                Duration.ofMillis(configService.getRequestTimeout()));

        this.vaultTemplate = new VaultTemplate(
                endpoint,
                new TokenAuthentication(configService.getToken())
        );

        String mountPath = configService.getMountPath();
        String kvVersion = configService.getKvVersion();

        if ("2".equals(kvVersion)) {
            this.kvOperations = vaultTemplate.opsForKeyValue(
                        mountPath,
                        VaultKeyValueOperations.KeyValueBackend.KV_2);
        }
        else { 
            this.kvOperations = vaultTemplate.opsForKeyValue(
                        mountPath,
                        VaultKeyValueOperations.KeyValueBackend.KV_1);
        }
    }

    /**
     * Retrieves a secret from OpenBao by username.
     *
     * @param username
     *     The Guacamole username to look up in OpenBao.
     *
     * @return
     *     The JSON response from OpenBao in the form of a Map.
     *
     * @throws GuacamoleException
     *     If the secret cannot be retrieved from OpenBao.
     */
    public Map<String, Object> getSecret(String username)
            throws GuacamoleException {

        try {
            logger.info("Fetching secret from OpenBao: key={}", username);

            VaultResponse response = kvOperations.get(username);

            if (response == null || response.getData() == null) {
                throw new GuacamoleServerException(
                        "Secret not found in OpenBao for username: " + username);
            }

            return response.getData();

        } catch (VaultException e) {
            logger.error("Failed to retrieve secret from OpenBao", e);
            throw new GuacamoleServerException(
                    "Failed to retrieve secret from OpenBao", e);
        }
    }

    /**
     * Extracts the password field from an OpenBao KV v2 response.
     *
     * @param response
     *     The JSON response from OpenBao in the form of a Map<String,Object>.
     *
     * @return
     *     The password string, or null if not found.
     */
    public String extractPassword(Map<String, Object> sresponse) {

        if (response == null) {
            return null;
        }

        Object password = response.get("password");

        if (password instanceof String) {
            return (String) password;
        }

        logger.warn("Password field not found in OpenBao secret");
        return null;
    }
}

but completely untested could reimplement your getSecret and extractPassword methods using a Map rather than a JsonObject

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the detailed proposal — the Spring Vault version is definitely cleaner for future feature work like AppRole or KV listing.

For this initial contribution I'd like to keep the footprint minimal: the current hand-rolled client only needs httpclient5 + gson (both already familiar dependencies in the Guacamole tree) and doesn't pull in the Spring runtime. Moving to org.springframework.vault is a larger decision that touches dependency policy, so I'd rather propose it as a follow-up PR where it can be discussed on its own merits.

I have, however, addressed the underlying concerns that motivated the suggestion: the HttpClient is now constructed once and reused, exceptions are caught more narrowly, and AppRole authentication is supported.

@adb014

/**
* Gson instance for JSON parsing.
*/
private final Gson gson = new Gson();
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SHoudn't this be static..

Copy link
Copy Markdown

@adb014 adb014 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Propose OpenBaoClient based on org.springframework.vault

* @throws GuacamoleException
* If the property is not defined in guacamole.properties.
*/
public String getToken() throws GuacamoleException {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn’t it be better to use an openbao roleID and secretID to obtain the token rather than supply it directly? The token typically has a shorter lifetime

Copy link
Copy Markdown

@adb014 adb014 Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you do then code in my proposed spring implementation is changed to.

this.vaultTemplate = new VaultTemplate(endpoint,
    new AppRoleAuthentication(confService.getRoleID(),
        new AppRoleAuthenticationOptions()
            .setSecretId(confService.getSecretID())));

@adb014
Copy link
Copy Markdown

adb014 commented Apr 16, 2026

Shouldn't This merge with the PR #1116 as Hashicorp and OpenBao use essentially the same API

Comment on lines +106 to +113
// Handle GUAC_USERNAME token - return the Guacamole username
if ("GUAC_USERNAME".equals(token)) {
logger.info("getValue() returning username: '{}'", username);
return CompletableFuture.completedFuture(username);
}

// Handle OPENBAO_SECRET token - fetch password from OpenBao
if ("OPENBAO_SECRET".equals(token)) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only handles '${GUAC_USERNAME}" and "${OPENBAO_SECRET}" so there is only a single key/value pair allowed . This should do something like

static String user_password_subpath = "userpasswords/";
String key;
if (token.toLowerCase().startsWith("openbao:") || ){
     key = token.subString(8);
}
else if token.equals("OPENBAO_SECRET") {
    key = user_password_subpath + username;
}
if (key) {
...
     JsonObject response = openBaoClient.getSecret(key);

This would also allow passwords like "${openbao:/path/to/secret}" where the path is under the mount path used. "${OPENBAO_SECRET}" should probably be "${OPENBAO_PASSWORD}" for consistency with "${GAUC_PASSWORD}"

Also not sure we get here with " ${GUAC_USERNAME}" value, or if we do we should respond with an empty string and allow Guacamole itself to replace the value

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've implemented that additively in this new commit, so arbitrary secret paths are now supported alongside the existing per-user lookup.
I'd like to avoid renaming ${OPENBAO_SECRET} in this PR to "${OPENBAO_PASSWORD}"

// Add GUAC_USERNAME token (always available)
tokens.put("GUAC_USERNAME", CompletableFuture.completedFuture(username));

// Add OPENBAO_SECRET token (fetch from OpenBao)
Copy link
Copy Markdown

@adb014 adb014 Apr 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you are using org.springramework.vault this should use the VaultKeyValueOperations.list function to first obtain a list of all of the keys under a specific mount point and then obtain a Map<String,String> of the keys. If you add a method

public List<String> listKeys(String path) {
   return kvOperations.list(path);
}

to my proposed replacement for OpenBaoClient, then you could do something like
like

List<Strings> keys = OpenBaoClient.listKeys(""); 
Map<String, String> secrets = new HashMap<>();

for (String key : keys) {
    Map<String,Object> response = openBaoClient.getSecret(key);
    String password = openBaoClient.extractPassword(response);
    if (response != null && response.getData() != null) {
        Object value = response.getData().get("value");
        if (value != null) {
            secrets.put(key, value.toString());
        }
    }
}

where keys and the lists keys

Copy link
Copy Markdown

@adb014 adb014 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was a bit mean when I suggested AppRole authentication method.. The AppRole method usually has a third party broker. See the How (and why) to use AppRole correctly in Hashicorp Vault. The idea is that the SecretID is extremely short lived and very limited in the secrets it can access and generated for each request of Guacamole to OpenBao.

What you've implemented in fact assumes that the SecretID has a TTL of zero, has access to all of the secrets needed by Guacamole, and is used directly by Guacamole without a Broker. Essentially you've recreated the Username/Password authentication method with AppRole. On reflection that's not a bad thing, but in that case use username/password instead of AppRole.

Here is what I know think should be done done

  • Guacamole should only support a Token and Username/Password authentication method
  • The username/password authentication method you supplied can be used entirely within Guacamole, including leasing management on the token renewals
  • The token authenication should allow either a token directly in guacamole.properties and this token must have a TTL of zero (The token from the sky in the above article), but also that the token is read from a file.
  • This file should be written to be by a Vault Agent with the tokens based on the AppRole.. Guacamole doesn't need to known about the AppRole at all.
  • The token read from the file should be possible to have a non zero TTL. Guacamole should be aware of the TTL and if a request would fail due to an expired TTL it should reread the file that will have been updated by the agent and reauthenticate. This could be done with a token lifecycle management in org.springframework.vault for example

I've got a lot of this code written, but completely untested at this point. I didn't want to send it till it was tested a bit, but as you are working on this at the same time as me, I should share what I have to spread the effort... I'll tidy my code up a bit and share it with you at least for discussion

@adb014
Copy link
Copy Markdown

adb014 commented Apr 24, 2026

I won't make a pull request of this yet, because it is very much a work in progress, but the "openbao" branch of my fork has what I think this module should look like.. At this point it compiles, but its completely untested (I haven't even run it once). But I wanted to get it out there for discussion with you.

My version can use Guacamole tokens of the form vault://<MOUNT PATH>/<ROLE>/<SECRET>, where <MOUNT PATH> is a valid mount path on the vault server using one of 4 possible secret engines

  • Key-Values : The engine you are using
  • LDAP : The secret engine for ldap managed accounts, allowing password rotation by OpenBao
  • SSH : The ssh engine, allowing one-time password with a helper function of signed certificate access
  • Database: Allowing the password and account name that guacamole itself uses to access its database to be stored and rotated by the Vault

The type of secret engine is automatically detected by reading "sys/mounts" from the vault. So the role assigned to Guacamole needs read access in the vault to "sys/mounts". This also means it can support both KV_1 and KV_2 key-values stores intrinsically and is fully Hashicorp/OpenBao compatible via its use of org.springframework.vault

Two authentication methods are supported

  • Token Authentication : The token can be a standard Base62 encoded service token of the Vault or a token in a file. If the token is in a file the life cycle management of the token taken into account and the file is reread on renewal. This allows this file to be a sink of a VaultAgent allowing AuthRole login, etc to be implemented with the VaultAgent as the broker running locally on the same machine as the guacamole client
  • Username/Password : as you'd expect

I've attempted to write the document for guacamole-manual included in the "doc/" sub-directory. This still needs lots of work and lots of testing. But I think its more complete than your code or the pull request #1116. I'll do some testing for my use cases, but I'd love to collaborate on getting this to work.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants