Apache Guacamole¶
Apache Guacamole remote desktop gateway with a PostgreSQL database, optional single sign-on, session recording to object storage and a locked-down kiosk browser desktop
| Chart | guacamole |
| Version | 0.1.1 |
| Application version | 1.6.0 |
| Kind | Application |
| Source | charts/guacamole |
The values are listed under Values, the changes per version under Changelog.
Purpose¶
Runs Apache Guacamole, a remote desktop gateway in the browser: users open VNC, RDP or SSH desktops from a web page and receive only pixels. The chart brings the web application, the guacd proxy, a PostgreSQL database (CloudNativePG) and the connections themselves, which are declared in values. Optionally users sign in at an OpenID Connect issuer, and every session is recorded, played back from the connection history and copied to object storage.
Install¶
helm repo add grncloud https://apps.openshift.eu
helm install guacamole grncloud/guacamole
Read the address and the administrator password:
oc get route guacamole-guacamole -o jsonpath='https://{.spec.host}/{"\n"}'
oc get secret guacamole-guacamole -o jsonpath='{.data.adminPassword}' | base64 -d
Objects are named <release>-guacamole. The administrator is guacadmin; its password is set from the Secret on every install and upgrade, so a password changed in the web interface is reset.
route.hostname(default: empty): Hostname of Guacamole. Empty: OpenShift assigns one. Set it at install; a tenant cannot change the hostname of an existing Route.route.labels(default: empty): Extra labels on the Route, for example to select it for a router shard or a platform policy.connections(default: none): Connections the chart manages. Each hasname,protocol,hostname,port,parameters(Guacamole connection parameters, values as strings),secretParameters(parameter name to a key of the Secret<release>-guacamole, for examplepassword: vncPassword),usersandgroups(group names of the issuer, asoidc.groupsClaimgives them, whose members get READ on the connection).disable-copyanddisable-pasteare"true"unlessparameterssets them, so no clipboard crosses a connection by default. The chart sets exactly these parameters and gives exactly these users READ on the connection, with no other permission. A connection removed from the list stays in the database; empty itsusersto close it.oidc.enabled(default: false): Sign in at an OpenID Connect issuer.oidc.issuerUrl,oidc.clientId: Your issuer and client. Required withoidc.enabled, together withroute.hostname.oidc.authorizationEndpoint,oidc.jwksEndpoint(default: the Keycloak addresses underoidc.issuerUrl): Endpoints of another issuer.oidc.usernameClaim(default: preferred_username): Token claim that holds the user name. It must equal the names inconnections[].users.oidc.groupsClaim(default: groups): Token claim that holds the user's group names. Guacamole matches them with the user groups the chart creates foradminGroupsandconnections[].groups, so a member needs no database account.oidc.primary(default: true): Send users straight to the issuer, without the Guacamole login form (EXTENSION_PRIORITY=openid). With false they see the login form with a link to the issuer. Either wayguacadminsigns in with its password through the API, see the Delivery instructions.admins(default: none): User names, asoidc.usernameClaimgives them, that get every system permission (administer, audit, create connections, groups, sharing profiles, users and user groups) and READ on every connection. The chart marks them with the user attributeguacamole-chart-admin; a name removed from the list loses those permissions and the mark on the next upgrade. A name may not be inadminsand in a connection'susersat once, andguacadminmay not be in it: it stays the break-glass administrator.adminGroups(default: none): Group names, asoidc.groupsClaimgives them, whose members get the same permissions asadmins. The chart creates each as a Guacamole user group with the user group attributeguacamole-chart-admin; a group removed from the list loses the permissions and the mark on the next upgrade.recording.enabled(default: true): Record every session of a managed connection to a volume ofrecording.size(default: 10Gi). The administrator plays a recording from the connection history.recording.s3.enabled(default: false): Copy the recordings every 5 minutes to a bucket from an ObjectBucketClaim of classrecording.s3.storageClassName. A recording is never deleted from the bucket by the chart.watermark.enabled(default: false): Tile the signed-in user name and the time (UTC, refreshed every minute) faintly over every page, remote desktop included, so a photo or screenshot of the screen shows who took it and when. The overlay lets every click and key through. The client address is not shown: the browser does not know it. The extension isfiles/watermark.jar, built byci/build-watermark.shfromfiles/watermark.kiosk.enabled(default: false): A Firefox desktop in kiosk mode behind a VNC server, Service<release>-kioskon port 5900, reachable from the Guacamole pod only. Add a connection withprotocol: vnc,hostname: <release>-kiosk,port: 5900andsecretParameters: {password: vncPassword}. The image is built in the project fromfiles/kiosk/Containerfile(Fedora, Firefox, TigerVNC) unlesskiosk.imagenames a prebuilt one.kiosk.replicas(default: 1): 0 stops the desktop and keeps its profile volume. At most 1: one Firefox owns a profile.kiosk.homepage(default: about:blank): First page.kiosk.allowedUrls(default: none): The only addresses Firefox opens, as match patterns such ashttps://code.example.com/*. Everything else,file://included, is blocked. Firefox match patterns take no port:http://10.0.0.1/*matches every port of that host, and an entry with a port fails the render. Firefox policy also turns off developer tools,about:config, printing, extensions, password reveal, file selection dialogs and Firefox accounts. Without a file dialog, Save Page As (Ctrl+S) and the password export save nothing; downloads go without a prompt to a read-only directory and fail. Firefox has no policy that refuses downloads.kiosk.connectTimeout(default: 5): Seconds Firefox waits for a TCP connection, locked by policy; Firefox itself waits 90. The egress firewall drops a connection to a host it does not allow without an answer, so a page that loads a stylesheet or font from such a host (a login theme that imports a web font, for example) stays blank, or keeps showing the previous page, until the wait ends. The address filter does not cover stylesheets, fonts or scripts, only pages and frames.kiosk.policies(default: none): Further Firefox enterprise policies (https://mozilla.github.io/policy-templates/), merged with the ones the chart sets. Where both set the same key, the chart value stays, so this cannot loosen the lock-down.kiosk.preferences(default: none): Further Firefox preferences as name and value, each locked. A preference the chart sets keeps the chart value. Example:browser.newtabpage.pinnedlists the sites that Ctrl+L offers, andbrowser.newtabpage.activity-stream.default.sites: ""removes the Firefox defaults from that list.kiosk.egressFirewall.enabled(default: true),kiosk.egressFirewall.allowedDnsNames,kiosk.egressFirewall.allowedCidrs(default: none): An EgressFirewall nameddefaultthat lets pods of the project reach only these names and address ranges outside the cluster.kiosk.geometry(default: 1600x900): Size of the desktop. Firefox opens at exactly this size: the desktop has no window manager, which full-screen kiosk mode would need.kiosk.profileSize(default: 1Gi): Volume of the Firefox profile, which keeps saved logins and persistent cookies across restarts. Form history is off, and every start removes the typed form values and the tabs of the previous run, so the desktop always opens onkiosk.homepagealone.database.instances(default: 1),database.size(default: 1Gi): The PostgreSQL cluster.secrets.adminPassword,secrets.vncPassword(default: empty): Generated once when empty.secrets.type(default: kubernetes):externalSecretreads the secrets from a secret store.
Delivery instructions¶
- A hostname outside the default application domain needs a DNS record pointing at the platform's ingress. Ask the platform team for the record; the certificate follows automatically once it resolves.
- Guacamole uses the OpenID Connect implicit flow and needs no client secret. The client at the issuer must allow the implicit flow and the redirect address
https://<hostname>/. Ask the platform team for a client at the platform's issuer, or use your own issuer. - A user signed in through the issuer sees only the connections whose
userslist holds their name fromoidc.usernameClaim, or whosegroupslist holds one of their groups fromoidc.groupsClaim. Any other user of the issuer can sign in and sees no connection. - The connections run on each install and upgrade through a Job that runs after the other objects; under Argo CD it is a PostSync hook. A change in the web interface to a managed connection is undone by the next run.
- With
recording.s3.enabled, give the bucket a lifecycle rule that expires recordings afterrecording.s3.expiryDaysdays; an ObjectBucketClaim cannot carry one. With the credentials from the Secret and ConfigMap<release>-guacamole-recordings:aws s3api put-bucket-lifecycle-configuration --endpoint-url <endpoint> --bucket <BUCKET_NAME> --lifecycle-configuration '{"Rules":[{"ID":"expire","Status":"Enabled","Filter":{},"Expiration":{"Days":180}}]}' - The recordings volume and the bucket are kept when the chart is uninstalled. Delete them yourself when the recordings are no longer needed.
- The kiosk EgressFirewall applies to every pod of the project, not only to the desktop, and a project holds one EgressFirewall only. With
oidc.enabled, the issuer host (and the host ofoidc.jwksEndpoint, when set) must be inkiosk.egressFirewall.allowedDnsNames, otherwise Guacamole cannot fetch the signing keys; the chart refuses to render without it. Withrecording.s3.enabled, the bucket endpoint comes from the ObjectBucketClaim at run time, so the chart cannot check it: when the endpoint is outside the cluster, add its host or address range yourself, or the copy to the bucket fails. The kiosk build runs in the same project and pullsregistry.fedoraproject.org/fedoraand Fedora packages from mirrors that change per request, so it fails behind the EgressFirewall: build the image before the EgressFirewall exists, lift it for the length of a rebuild (oc start-build <release>-kiosk), or setkiosk.imageto a prebuilt image. On a hosted control plane cluster, pods reach the Kubernetes API through a node-local proxy address outside the cluster networks (for example172.20.0.1): add it tokiosk.egressFirewall.allowedCidrs, or the CloudNativePG instance manager in the database pods cannot reach the API server and the database never becomes ready. Creating an EgressFirewall can need a permission your project role lacks: ask the platform team, or setkiosk.egressFirewall.enabled=falseand have the platform team provide it. - With
oidc.primary, the login page redirects to the issuer, soguacadminsigns in through the API:curl -s -d username=guacadmin --data-urlencode password@<file> https://<hostname>/api/tokensreturns anauthToken; openhttps://<hostname>/?token=<authToken>. - Do not save the password of a single sign-on account in the kiosk. An operator signs that account in once in a desktop session instead, and the kiosk keeps the session until the desktop restarts: a single sign-on session usually lives in a session cookie, and the kiosk restores no session. Sign the account in again after a restart. Saved logins are only for accounts of systems that are reachable from inside the kiosk's network alone. Firefox fills them in without showing the password, but it has no policy that hides the saved-logins page
about:loginswhile keeping autofill (PasswordManagerEnabled=falsealso stops autofill), and the kiosk reaches it: the address bar is hidden but Ctrl+L still takes an address, andabout:loginsis not subject tokiosk.allowedUrls. Its Copy button puts a saved password on the kiosk clipboard, and pasting it into any text field shows it. Treat every saved password as known to the kiosk user. - With
secrets.type=externalSecret, create the items named insecrets.externalSecret.itemsin your vault collection, each with the value in the password field, and setsecrets.externalSecret.storeNameto your store.