diff --git a/extensions/guacamole-vault/modules/guacamole-vault-openbao/.ratignore b/extensions/guacamole-vault/modules/guacamole-vault-openbao/.ratignore new file mode 100644 index 0000000000..e69de29bb2 diff --git a/extensions/guacamole-vault/modules/guacamole-vault-openbao/README.md b/extensions/guacamole-vault/modules/guacamole-vault-openbao/README.md new file mode 100644 index 0000000000..ba6996d49a --- /dev/null +++ b/extensions/guacamole-vault/modules/guacamole-vault-openbao/README.md @@ -0,0 +1,209 @@ +# OpenBao Vault Extension for Apache Guacamole + +This extension integrates Apache Guacamole with [OpenBao](https://openbao.org/) 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: +```json +{ + "data": { + "data": { + "username": "user1", + "password": "SecretPassword123" + } + } +} +``` + +### Guacamole Configuration + +Add the following properties to `guacamole.properties`: + +```properties +# OpenBao server URL (required) +openbao-server-url: http://openbao.example.com:8200 + +# OpenBao authentication token (required) +openbao-token: s.YourTokenHere + +# KV mount path (optional, default: guacamole-credentails) +openbao-mount-path: guacamole-credentails +``` + +**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 from OpenBao +- **`${GUAC_USERNAME}`**: Replaced with the logged-in Guacamole username + +Example RDP connection: +- Username: `${GUAC_USERNAME}` +- Password: `${OPENBAO_SECRET}` +- Hostname: `192.168.1.100` + +## 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: + +```bash +cd extensions/guacamole-vault +mvn clean package +``` + +The built extension will be located at: +``` +modules/guacamole-vault-openbao/target/guacamole-vault-openbao-.jar +``` + +## Installation + +1. Copy the built JAR to the Guacamole extensions directory: + ```bash + 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: + ```properties + 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**: + ```bash + # 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**: + ```properties + 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: +- Apache Guacamole: https://guacamole.apache.org/ +- OpenBao: https://openbao.org/ +- Issue Tracker: https://issues.apache.org/jira/browse/GUACAMOLE/ diff --git a/extensions/guacamole-vault/modules/guacamole-vault-openbao/pom.xml b/extensions/guacamole-vault/modules/guacamole-vault-openbao/pom.xml new file mode 100644 index 0000000000..41fc201711 --- /dev/null +++ b/extensions/guacamole-vault/modules/guacamole-vault-openbao/pom.xml @@ -0,0 +1,70 @@ + + + + + 4.0.0 + org.apache.guacamole + guacamole-vault-openbao + jar + guacamole-vault-openbao + http://guacamole.apache.org/ + + + org.apache.guacamole + guacamole-vault + ${revision} + ../../ + + + + + + + org.apache.guacamole + guacamole-ext + + + + + org.apache.guacamole + guacamole-vault-base + ${revision} + + + + + org.apache.httpcomponents.client5 + httpclient5 + 5.2.1 + + + + + com.google.code.gson + gson + 2.10.1 + + + + + diff --git a/extensions/guacamole-vault/modules/guacamole-vault-openbao/src/main/java/org/apache/guacamole/vault/openbao/OpenBaoAuthenticationProvider.java b/extensions/guacamole-vault/modules/guacamole-vault-openbao/src/main/java/org/apache/guacamole/vault/openbao/OpenBaoAuthenticationProvider.java new file mode 100644 index 0000000000..075335812d --- /dev/null +++ b/extensions/guacamole-vault/modules/guacamole-vault-openbao/src/main/java/org/apache/guacamole/vault/openbao/OpenBaoAuthenticationProvider.java @@ -0,0 +1,54 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.guacamole.vault.openbao; + +import org.apache.guacamole.GuacamoleException; +import org.apache.guacamole.vault.VaultAuthenticationProvider; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * OpenBao authentication provider that retrieves RDP passwords from OpenBao. + * This provider integrates with the Guacamole vault framework to automatically + * fetch passwords from OpenBao based on the logged-in username. + */ +public class OpenBaoAuthenticationProvider extends VaultAuthenticationProvider { + + /** + * Logger for this class. + */ + private static final Logger logger = LoggerFactory.getLogger(OpenBaoAuthenticationProvider.class); + + /** + * Creates a new OpenBaoAuthenticationProvider. + * + * @throws GuacamoleException + * If an error occurs during initialization. + */ + public OpenBaoAuthenticationProvider() throws GuacamoleException { + super(new OpenBaoAuthenticationProviderModule()); + logger.info("OpenBaoAuthenticationProvider initialized"); + } + + @Override + public String getIdentifier() { + return "openbao"; + } +} diff --git a/extensions/guacamole-vault/modules/guacamole-vault-openbao/src/main/java/org/apache/guacamole/vault/openbao/OpenBaoAuthenticationProviderModule.java b/extensions/guacamole-vault/modules/guacamole-vault-openbao/src/main/java/org/apache/guacamole/vault/openbao/OpenBaoAuthenticationProviderModule.java new file mode 100644 index 0000000000..2092d13417 --- /dev/null +++ b/extensions/guacamole-vault/modules/guacamole-vault-openbao/src/main/java/org/apache/guacamole/vault/openbao/OpenBaoAuthenticationProviderModule.java @@ -0,0 +1,78 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.guacamole.vault.openbao; + +import com.google.inject.Scopes; +import org.apache.guacamole.GuacamoleException; +import org.apache.guacamole.vault.VaultAuthenticationProviderModule; +import org.apache.guacamole.vault.conf.VaultConfigurationService; +import org.apache.guacamole.vault.openbao.conf.OpenBaoConfigurationService; +import org.apache.guacamole.vault.openbao.secret.OpenBaoClient; +import org.apache.guacamole.vault.openbao.secret.OpenBaoSecretService; +import org.apache.guacamole.vault.openbao.user.OpenBaoAttributeService; +import org.apache.guacamole.vault.openbao.user.OpenBaoDirectoryService; +import org.apache.guacamole.vault.secret.VaultSecretService; +import org.apache.guacamole.vault.conf.VaultAttributeService; +import org.apache.guacamole.vault.user.VaultDirectoryService; + +/** + * Guice module for configuring OpenBao vault integration. + * Binds the OpenBao-specific implementations to the vault base interfaces. + */ +public class OpenBaoAuthenticationProviderModule extends VaultAuthenticationProviderModule { + + /** + * Creates a new OpenBaoAuthenticationProviderModule. + * + * @throws GuacamoleException + * If an error occurs while reading guacamole.properties. + */ + public OpenBaoAuthenticationProviderModule() throws GuacamoleException { + super(); + } + + @Override + protected void configureVault() { + + // Bind configuration service + bind(VaultConfigurationService.class) + .to(OpenBaoConfigurationService.class) + .in(Scopes.SINGLETON); + + // Bind secret service + bind(VaultSecretService.class) + .to(OpenBaoSecretService.class) + .in(Scopes.SINGLETON); + + // Bind attribute service + bind(VaultAttributeService.class) + .to(OpenBaoAttributeService.class) + .in(Scopes.SINGLETON); + + // Bind directory service + bind(VaultDirectoryService.class) + .to(OpenBaoDirectoryService.class) + .in(Scopes.SINGLETON); + + // Bind OpenBao client + bind(OpenBaoClient.class) + .in(Scopes.SINGLETON); + } +} diff --git a/extensions/guacamole-vault/modules/guacamole-vault-openbao/src/main/java/org/apache/guacamole/vault/openbao/conf/OpenBaoConfig.java b/extensions/guacamole-vault/modules/guacamole-vault-openbao/src/main/java/org/apache/guacamole/vault/openbao/conf/OpenBaoConfig.java new file mode 100644 index 0000000000..d7e2648ae9 --- /dev/null +++ b/extensions/guacamole-vault/modules/guacamole-vault-openbao/src/main/java/org/apache/guacamole/vault/openbao/conf/OpenBaoConfig.java @@ -0,0 +1,64 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.guacamole.vault.openbao.conf; + +import org.apache.guacamole.properties.StringGuacamoleProperty; + +/** + * Configuration properties for OpenBao vault integration. + */ +public class OpenBaoConfig { + + /** + * OpenBao server URL (e.g., "http://localhost:8200"). + * This property is REQUIRED and must be configured in guacamole.properties. + */ + public static final StringGuacamoleProperty OPENBAO_SERVER_URL = + new StringGuacamoleProperty() { + @Override + public String getName() { + return "openbao-server-url"; + } + }; + + /** + * OpenBao authentication token. + * This property is REQUIRED for authenticating with OpenBao. + */ + public static final StringGuacamoleProperty OPENBAO_TOKEN = + new StringGuacamoleProperty() { + @Override + public String getName() { + return "openbao-token"; + } + }; + + /** + * OpenBao KV secrets engine mount path (default: "rdp-creds"). + * This is the mount point where RDP credentials are stored. + */ + public static final StringGuacamoleProperty OPENBAO_MOUNT_PATH = + new StringGuacamoleProperty() { + @Override + public String getName() { + return "openbao-mount-path"; + } + }; +} diff --git a/extensions/guacamole-vault/modules/guacamole-vault-openbao/src/main/java/org/apache/guacamole/vault/openbao/conf/OpenBaoConfigurationService.java b/extensions/guacamole-vault/modules/guacamole-vault-openbao/src/main/java/org/apache/guacamole/vault/openbao/conf/OpenBaoConfigurationService.java new file mode 100644 index 0000000000..d3a898e5a9 --- /dev/null +++ b/extensions/guacamole-vault/modules/guacamole-vault-openbao/src/main/java/org/apache/guacamole/vault/openbao/conf/OpenBaoConfigurationService.java @@ -0,0 +1,122 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.guacamole.vault.openbao.conf; + +import com.google.inject.Inject; +import org.apache.guacamole.GuacamoleException; +import org.apache.guacamole.environment.Environment; +import org.apache.guacamole.vault.conf.VaultConfigurationService; + +/** + * Service for retrieving OpenBao configuration from guacamole.properties. + */ +public class OpenBaoConfigurationService extends VaultConfigurationService { + + /** + * The Guacamole server environment. + */ + @Inject + private Environment environment; + + /** + * Creates a new OpenBaoConfigurationService. + */ + public OpenBaoConfigurationService() { + super("openbao-token-mapping.yml", "guacamole.properties.openbao"); + } + + /** + * Returns the OpenBao server URL. + * + * @return The OpenBao server URL (e.g., "http://localhost:8200"). + * @throws GuacamoleException + * If the property is not defined in guacamole.properties. + */ + public String getServerUrl() throws GuacamoleException { + return environment.getRequiredProperty(OpenBaoConfig.OPENBAO_SERVER_URL); + } + + /** + * Returns the OpenBao authentication token. + * + * @return The OpenBao authentication token. + * @throws GuacamoleException + * If the property is not defined in guacamole.properties. + */ + public String getToken() throws GuacamoleException { + return environment.getRequiredProperty(OpenBaoConfig.OPENBAO_TOKEN); + } + + /** + * Returns the OpenBao KV secrets engine mount path. + * + * @return The mount path (default: "rdp-creds"). + * @throws GuacamoleException + * If an error occurs reading the property. + */ + public String getMountPath() throws GuacamoleException { + return environment.getProperty( + OpenBaoConfig.OPENBAO_MOUNT_PATH, + "rdp-creds" + ); + } + + /** + * Returns the OpenBao KV version. + * Hardcoded to "2" for KV v2 secrets engine. + * + * @return The KV version "2". + */ + public String getKvVersion() { + return "2"; + } + + /** + * Returns the connection timeout in milliseconds. + * Hardcoded to 5000ms (5 seconds). + * + * @return The connection timeout of 5000ms. + */ + public int getConnectionTimeout() { + return 5000; + } + + /** + * Returns the request timeout in milliseconds. + * Hardcoded to 10000ms (10 seconds). + * + * @return The request timeout of 10000ms. + */ + public int getRequestTimeout() { + return 10000; + } + + @Override + public boolean getSplitWindowsUsernames() throws GuacamoleException { + // Not needed for OpenBao - return false + return false; + } + + @Override + public boolean getMatchUserRecordsByDomain() throws GuacamoleException { + // Not needed for OpenBao - return false + return false; + } +} diff --git a/extensions/guacamole-vault/modules/guacamole-vault-openbao/src/main/java/org/apache/guacamole/vault/openbao/secret/OpenBaoClient.java b/extensions/guacamole-vault/modules/guacamole-vault-openbao/src/main/java/org/apache/guacamole/vault/openbao/secret/OpenBaoClient.java new file mode 100644 index 0000000000..a466572a88 --- /dev/null +++ b/extensions/guacamole-vault/modules/guacamole-vault-openbao/src/main/java/org/apache/guacamole/vault/openbao/secret/OpenBaoClient.java @@ -0,0 +1,164 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.guacamole.vault.openbao.secret; + +import com.google.gson.Gson; +import com.google.gson.JsonObject; +import com.google.inject.Inject; +import org.apache.guacamole.GuacamoleException; +import org.apache.guacamole.GuacamoleServerException; +import org.apache.guacamole.vault.openbao.conf.OpenBaoConfigurationService; +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; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; + +/** + * Client for communicating with OpenBao REST API. + */ +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; + + /** + * Gson instance for JSON parsing. + */ + private final Gson gson = new Gson(); + + /** + * Retrieves a secret from OpenBao by username. + * + * @param username + * The Guacamole username to look up in OpenBao. + * + * @return + * The JSON response from OpenBao. + * + * @throws GuacamoleException + * If the secret cannot be retrieved from OpenBao. + */ + public JsonObject getSecret(String username) throws GuacamoleException { + + String serverUrl = configService.getServerUrl(); + String token = configService.getToken(); + String mountPath = configService.getMountPath(); + String kvVersion = configService.getKvVersion(); + + // Build the API path based on KV version + // KV v2: /v1/{mount-path}/data/{username} + // KV v1: /v1/{mount-path}/{username} + String apiPath; + if ("2".equals(kvVersion)) { + apiPath = String.format("/v1/%s/data/%s", mountPath, username); + } else { + apiPath = String.format("/v1/%s/%s", mountPath, username); + } + + String fullUrl = serverUrl + apiPath; + + logger.info("Fetching secret from OpenBao: {}", fullUrl); + + try (CloseableHttpClient httpClient = HttpClients.createDefault()) { + + HttpGet httpGet = new HttpGet(fullUrl); + httpGet.setHeader("X-Vault-Token", token); + httpGet.setHeader("Accept", "application/json"); + + // Set timeouts + httpGet.setConfig(org.apache.hc.client5.http.config.RequestConfig.custom() + .setConnectionRequestTimeout(Timeout.ofMilliseconds(configService.getConnectionTimeout())) + .setResponseTimeout(Timeout.ofMilliseconds(configService.getRequestTimeout())) + .build()); + + org.apache.hc.core5.http.ClassicHttpResponse response = httpClient.executeOpen(null, httpGet, null); + try { + int statusCode = response.getCode(); + String responseBody = EntityUtils.toString(response.getEntity()); + + if (statusCode == 200) { + logger.info("OpenBao response status: {} - successfully retrieved password for {}", statusCode, username); + JsonObject jsonResponse = gson.fromJson(responseBody, JsonObject.class); + return jsonResponse; + } else if (statusCode == 404) { + logger.warn("Secret not found in OpenBao for username: {}", username); + throw new GuacamoleServerException("Secret not found in OpenBao for username: " + username); + } else if (statusCode == 403) { + logger.error("Permission denied accessing OpenBao. Check token permissions."); + throw new GuacamoleServerException("Permission denied accessing OpenBao. Check token permissions."); + } else { + logger.error("OpenBao returned error status {}: {}", statusCode, responseBody); + throw new GuacamoleServerException("OpenBao error (HTTP " + statusCode + "): " + responseBody); + } + } finally { + response.close(); + } + + } catch (IOException | org.apache.hc.core5.http.ParseException e) { + logger.error("Failed to communicate with OpenBao at {}: {}", fullUrl, e.getMessage()); + throw new GuacamoleServerException("Failed to communicate with OpenBao", e); + } + } + + /** + * Extracts the password field from an OpenBao KV v2 response. + * + * @param response + * The JSON response from OpenBao. + * + * @return + * The password string, or null if not found. + */ + public String extractPassword(JsonObject response) { + try { + // For KV v2: response.data.data.password + if (response.has("data")) { + JsonObject data = response.getAsJsonObject("data"); + if (data.has("data")) { + JsonObject innerData = data.getAsJsonObject("data"); + if (innerData.has("password")) { + return innerData.get("password").getAsString(); + } + } + } + + logger.warn("Password field not found in OpenBao response"); + return null; + + } catch (Exception e) { + logger.error("Error extracting password from OpenBao response", e); + return null; + } + } +} diff --git a/extensions/guacamole-vault/modules/guacamole-vault-openbao/src/main/java/org/apache/guacamole/vault/openbao/secret/OpenBaoSecretService.java b/extensions/guacamole-vault/modules/guacamole-vault-openbao/src/main/java/org/apache/guacamole/vault/openbao/secret/OpenBaoSecretService.java new file mode 100644 index 0000000000..f3a4efc98c --- /dev/null +++ b/extensions/guacamole-vault/modules/guacamole-vault-openbao/src/main/java/org/apache/guacamole/vault/openbao/secret/OpenBaoSecretService.java @@ -0,0 +1,172 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.guacamole.vault.openbao.secret; + +import com.google.gson.JsonObject; +import com.google.inject.Inject; +import org.apache.guacamole.GuacamoleException; +import org.apache.guacamole.net.auth.Connectable; +import org.apache.guacamole.net.auth.UserContext; +import org.apache.guacamole.protocol.GuacamoleConfiguration; +import org.apache.guacamole.token.TokenFilter; +import org.apache.guacamole.vault.secret.VaultSecretService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Future; + +/** + * OpenBao implementation of VaultSecretService. + * Retrieves RDP passwords from OpenBao based on the logged-in Guacamole username. + */ +public class OpenBaoSecretService implements VaultSecretService { + + /** + * Logger for this class. + */ + private static final Logger logger = LoggerFactory.getLogger(OpenBaoSecretService.class); + + /** + * Client for communicating with OpenBao. + */ + @Inject + private OpenBaoClient openBaoClient; + + /** + * Constructor that logs when the service is created. + */ + public OpenBaoSecretService() { + logger.info("OpenBaoSecretService initialized"); + } + + /** + * The token pattern for OpenBao secrets: ${OPENBAO_SECRET} + */ + public static final String OPENBAO_SECRET_TOKEN = "${OPENBAO_SECRET}"; + + /** + * The token pattern for Guacamole username: ${GUAC_USERNAME} + */ + public static final String GUAC_USERNAME_TOKEN = "${GUAC_USERNAME}"; + + @Override + public String canonicalize(String token) { + // Return the canonical form for tokens we recognize + if (token == null) + return null; + + // Remove ${} wrapper and return just the token name + if (OPENBAO_SECRET_TOKEN.equals(token)) { + return "OPENBAO_SECRET"; + } + + if (GUAC_USERNAME_TOKEN.equals(token)) { + return "GUAC_USERNAME"; + } + + // Not our token + return null; + } + + @Override + public Future getValue(String token) throws GuacamoleException { + // This method is called for simple token lookups without user context + logger.warn("getValue(String) called without user context - cannot determine username"); + return CompletableFuture.completedFuture(null); + } + + @Override + public Future getValue(UserContext userContext, Connectable connectable, String token) + throws GuacamoleException { + + logger.info("getValue() called with token: {}", token); + + // Get the logged-in Guacamole username + String username = userContext.self().getIdentifier(); + + // 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)) { + logger.info("Retrieving OpenBao secret for username: {}", username); + + try { + // Fetch the secret from OpenBao using the username + JsonObject response = openBaoClient.getSecret(username); + + // Extract the password field + String password = openBaoClient.extractPassword(response); + + if (password != null) { + logger.info("Successfully retrieved password from OpenBao for user: {} (length: {})", username, password.length()); + return CompletableFuture.completedFuture(password); + } else { + logger.warn("Password field not found in OpenBao for user: {}", username); + return CompletableFuture.completedFuture(null); + } + + } catch (GuacamoleException e) { + logger.error("Failed to retrieve secret from OpenBao for user: {}", username, e); + // Return null instead of throwing to allow connection attempt with empty password + return CompletableFuture.completedFuture(null); + } + } + + // Not a recognized token + logger.warn("Token '{}' not recognized, returning null", token); + return CompletableFuture.completedFuture(null); + } + + @Override + public Map> getTokens(UserContext userContext, + Connectable connectable, + GuacamoleConfiguration config, + TokenFilter tokenFilter) throws GuacamoleException { + + Map> tokens = new java.util.HashMap<>(); + String username = userContext.self().getIdentifier(); + + // Add GUAC_USERNAME token (always available) + tokens.put("GUAC_USERNAME", CompletableFuture.completedFuture(username)); + + // Add OPENBAO_SECRET token (fetch from OpenBao) + try { + JsonObject response = openBaoClient.getSecret(username); + String password = openBaoClient.extractPassword(response); + if (password != null) { + tokens.put("OPENBAO_SECRET", CompletableFuture.completedFuture(password)); + logger.info("Added token OPENBAO_SECRET with password from OpenBao (length: {})", password.length()); + } else { + logger.warn("Password not found in OpenBao for user: {}", username); + } + } catch (Exception e) { + logger.error("Failed to get secret from OpenBao for user: {}", username, e); + } + + logger.info("Returning {} tokens: {}", tokens.size(), tokens.keySet()); + return tokens; + } +} diff --git a/extensions/guacamole-vault/modules/guacamole-vault-openbao/src/main/java/org/apache/guacamole/vault/openbao/user/OpenBaoAttributeService.java b/extensions/guacamole-vault/modules/guacamole-vault-openbao/src/main/java/org/apache/guacamole/vault/openbao/user/OpenBaoAttributeService.java new file mode 100644 index 0000000000..06ee6679a6 --- /dev/null +++ b/extensions/guacamole-vault/modules/guacamole-vault-openbao/src/main/java/org/apache/guacamole/vault/openbao/user/OpenBaoAttributeService.java @@ -0,0 +1,58 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.guacamole.vault.openbao.user; + +import org.apache.guacamole.form.Form; +import org.apache.guacamole.vault.conf.VaultAttributeService; + +import java.util.Collection; +import java.util.Collections; + +/** + * OpenBao implementation of VaultAttributeService. + * Defines attributes that trigger OpenBao secret lookups. + */ +public class OpenBaoAttributeService implements VaultAttributeService { + + @Override + public Collection
getConnectionAttributes() { + // No additional connection attributes needed for OpenBao + // The password field in RDP connections will automatically use OPENBAO:password token + return Collections.emptyList(); + } + + @Override + public Collection getConnectionGroupAttributes() { + // No additional connection group attributes + return Collections.emptyList(); + } + + @Override + public Collection getUserAttributes() { + // No additional user attributes + return Collections.emptyList(); + } + + @Override + public Collection getUserPreferenceAttributes() { + // No additional user preference attributes + return Collections.emptyList(); + } +} diff --git a/extensions/guacamole-vault/modules/guacamole-vault-openbao/src/main/java/org/apache/guacamole/vault/openbao/user/OpenBaoDirectoryService.java b/extensions/guacamole-vault/modules/guacamole-vault-openbao/src/main/java/org/apache/guacamole/vault/openbao/user/OpenBaoDirectoryService.java new file mode 100644 index 0000000000..31c24a9a84 --- /dev/null +++ b/extensions/guacamole-vault/modules/guacamole-vault-openbao/src/main/java/org/apache/guacamole/vault/openbao/user/OpenBaoDirectoryService.java @@ -0,0 +1,80 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.guacamole.vault.openbao.user; + +import org.apache.guacamole.GuacamoleException; +import org.apache.guacamole.net.auth.ActiveConnection; +import org.apache.guacamole.net.auth.Connection; +import org.apache.guacamole.net.auth.ConnectionGroup; +import org.apache.guacamole.net.auth.Directory; +import org.apache.guacamole.net.auth.SharingProfile; +import org.apache.guacamole.net.auth.User; +import org.apache.guacamole.net.auth.UserGroup; +import org.apache.guacamole.vault.user.VaultDirectoryService; + +/** + * OpenBao implementation of VaultDirectoryService. + * Since OpenBao only provides secrets (not user/group/connection management), + * all directory methods simply pass through the underlying directories unchanged. + */ +public class OpenBaoDirectoryService extends VaultDirectoryService { + + @Override + public Directory getUserDirectory(Directory underlyingUserDirectory) + throws GuacamoleException { + // OpenBao doesn't manage users, just return the underlying directory + return underlyingUserDirectory; + } + + @Override + public Directory getUserGroupDirectory(Directory underlyingUserGroupDirectory) + throws GuacamoleException { + // OpenBao doesn't manage user groups, just return the underlying directory + return underlyingUserGroupDirectory; + } + + @Override + public Directory getConnectionDirectory(Directory underlyingConnectionDirectory) + throws GuacamoleException { + // OpenBao doesn't manage connections, just return the underlying directory + return underlyingConnectionDirectory; + } + + @Override + public Directory getConnectionGroupDirectory( + Directory underlyingConnectionGroupDirectory) throws GuacamoleException { + // OpenBao doesn't manage connection groups, just return the underlying directory + return underlyingConnectionGroupDirectory; + } + + @Override + public Directory getActiveConnectionDirectory( + Directory underlyingActiveConnectionDirectory) throws GuacamoleException { + // OpenBao doesn't manage active connections, just return the underlying directory + return underlyingActiveConnectionDirectory; + } + + @Override + public Directory getSharingProfileDirectory( + Directory underlyingSharingProfileDirectory) throws GuacamoleException { + // OpenBao doesn't manage sharing profiles, just return the underlying directory + return underlyingSharingProfileDirectory; + } +} diff --git a/extensions/guacamole-vault/modules/guacamole-vault-openbao/src/main/resource-templates/guac-manifest.json b/extensions/guacamole-vault/modules/guacamole-vault-openbao/src/main/resource-templates/guac-manifest.json new file mode 100644 index 0000000000..32b272cc52 --- /dev/null +++ b/extensions/guacamole-vault/modules/guacamole-vault-openbao/src/main/resource-templates/guac-manifest.json @@ -0,0 +1,12 @@ +{ + + "guacamoleVersion" : "${project.version}", + + "name" : "OpenBao Vault", + "namespace" : "openbao", + + "authProviders" : [ + "org.apache.guacamole.vault.openbao.OpenBaoAuthenticationProvider" + ] + +} diff --git a/extensions/guacamole-vault/pom.xml b/extensions/guacamole-vault/pom.xml index 14313ff7a4..1a2d005a56 100644 --- a/extensions/guacamole-vault/pom.xml +++ b/extensions/guacamole-vault/pom.xml @@ -45,6 +45,7 @@ modules/guacamole-vault-ksm + modules/guacamole-vault-openbao diff --git a/guacamole-docker/build.d/010-map-guacamole-extensions.sh b/guacamole-docker/build.d/010-map-guacamole-extensions.sh index 3804120e8a..da9708381b 100644 --- a/guacamole-docker/build.d/010-map-guacamole-extensions.sh +++ b/guacamole-docker/build.d/010-map-guacamole-extensions.sh @@ -115,5 +115,6 @@ map_extensions <<'EOF' guacamole-display-statistics................DISPLAY_STATISTICS_ guacamole-history-recording-storage.........RECORDING_ guacamole-vault/ksm.........................KSM_ + guacamole-vault/openbao.....................OPENBAO_ EOF