Skip to content

Instantly share code, notes, and snippets.

@programmerq
Last active August 25, 2025 15:14
Show Gist options
  • Save programmerq/6a6c127a94c9018f1a0503fa0140770a to your computer and use it in GitHub Desktop.
Save programmerq/6a6c127a94c9018f1a0503fa0140770a to your computer and use it in GitHub Desktop.
Custom App Access icons in Teleport

Usage

Add https://gist.githubusercontent.com/programmerq/6a6c127a94c9018f1a0503fa0140770a/raw/custom-icon.js as a user script using your preferred userscript extension. I used it with TamperMonkey on Chrome with success.

If your Teleport instance is not at teleport.sh, add the Teleport cluster URL as a user match.

To set a custom icon, base64 encode a 72x72 png image, and set it as the value for a label called teleport.dev/icon.

Resize and/or convert to png if needed. Many logos can also use a reduced color count:

% magick input.png -resize 72x72 -colors 8 output.png

Use a png minifier tool (optional):

% pngcrush output.png output-crush.png

base64 encode:

% cat output-crush.png | base64 -w 0
iVBORw0KGgoAAAANSUhEUgAAAEgAAABIBAMAAACnw650AAAAIGNIUk0AAHomAACAhAAA+gAAAIDoAAB1MAAA6mAAADqYAAAXcJy6UTwAAAASUExURQQnSARNcQRwmAWUrw+zyhrK2iUHv+EAAAAJcEhZcwAACxIAAAsSAdLdfvwAAAAHdElNRQfpAQIRNSYHLo/dAAABhklEQVRIx+2VUZKDIAyGxV5AtAeo2AN0DR6gJVxghftfZROw7u5MKD53mhn1gc8/fyDGpvnE+4Qe64yy2FWhE+K9Cp0R3YFsiJcK1BLj7/VsiI8KdGWoUp9KjP8qrmttDBAS6QIwoyQ3AGSZyFeMwXVC7QCMRQ4MQSxSJcbHLQjCAkRMAFiSmHg8GzSPevJZ6SYaJwipKG2zklDeiaFc0pS2Qdp3NuXxwdCAnE48QQAb8+sKfSj0wvALWYLkrmpheS5dCZL7RcH+/pnKvBXahO2mugeCuhdQEuh9XAu9suyFtxhXWam1DLkNmuWvYYD9xFoMs5GLA+9sztfjakwnbhMs3xrglqCHMRe5fe3T7YCzMdLgmOiAY15QExqKptCZqQmooRxDnWSJTK0spXpM0EWGLLpR696yb8nUkCF080T3ApSVAuaY5XTJOPAHTvdkSdpNmgSGt4EfRs62yfVzXn85e5QpJfobGapMuv6Vm3/5atO3OWAp5av/XNSBbCR14C/1ifeKH4j3bcOdElj9AAAAJXRFWHRkYXRlOmNyZWF0ZQAyMDI1LTAxLTAyVDE3OjQ4OjUwKzAwOjAwHMXuNQAAACV0RVh0ZGF0ZTptb2RpZnkAMjAyNS0wMS0wMlQxNzo0ODoyNCswMDowMJMSe4MAAAAodEVYdGRhdGU6dGltZXN0YW1wADIwMjUtMDEtMDJUMTc6NTM6MzgrMDA6MDD2YxSBAAAAFXRFWHRleGlmOkNvbG9yU3BhY2UANjU1MzUzewBuAAAAIHRFWHRleGlmOkNvbXBvbmVudHNDb25maWd1cmF0aW9uAC4uLmryoWQAAAAUdEVYdGV4aWY6RXhpZk9mZnNldAAxNjIwuJfX8QAAABV0RVh0ZXhpZjpFeGlmVmVyc2lvbgAwMjEwuHZWeAAAABl0RVh0ZXhpZjpGbGFzaFBpeFZlcnNpb24AMDEwMBLUKKwAAAAOdEVYdGV4aWY6TWFrZQBmbHV4EJmXhAAAABl0RVh0ZXhpZjpQaXhlbFhEaW1lbnNpb24AMTAyNPLFVh8AAAAZdEVYdGV4aWY6UGl4ZWxZRGltZW5zaW9uADEwMjRLPo33AAAAFnRFWHRleGlmOlVzZXJDb21tZW50AEFTQ0lJtONa3gAAABd0RVh0ZXhpZjpZQ2JDclBvc2l0aW9uaW5nADGsD4BjAAAAAElFTkSuQmCC

Add the base64 encoded value to your app definition:

app_service:
  apps:
    - name: dashboard
      uri: http://localhost:9000/
      rewrite:
        redirect:
        - localhost
      labels:
        teleport.dev/icon: iVBORw0KGgoAAAANSUhEUgAAAEgAAABIBAMAAACnw650AAAAIGNIUk0AAHomAACAhAAA+gAAAIDoAAB1MAAA6mAAADqYAAAXcJy6UTwAAAASUExURQQnSARNcQRwmAWUrw+zyhrK2iUHv+EAAAAJcEhZcwAACxIAAAsSAdLdfvwAAAAHdElNRQfpAQIRNSYHLo/dAAABhklEQVRIx+2VUZKDIAyGxV5AtAeo2AN0DR6gJVxghftfZROw7u5MKD53mhn1gc8/fyDGpvnE+4Qe64yy2FWhE+K9Cp0R3YFsiJcK1BLj7/VsiI8KdGWoUp9KjP8qrmttDBAS6QIwoyQ3AGSZyFeMwXVC7QCMRQ4MQSxSJcbHLQjCAkRMAFiSmHg8GzSPevJZ6SYaJwipKG2zklDeiaFc0pS2Qdp3NuXxwdCAnE48QQAb8+sKfSj0wvALWYLkrmpheS5dCZL7RcH+/pnKvBXahO2mugeCuhdQEuh9XAu9suyFtxhXWam1DLkNmuWvYYD9xFoMs5GLA+9sztfjakwnbhMs3xrglqCHMRe5fe3T7YCzMdLgmOiAY15QExqKptCZqQmooRxDnWSJTK0spXpM0EWGLLpR696yb8nUkCF080T3ApSVAuaY5XTJOPAHTvdkSdpNmgSGt4EfRs62yfVzXn85e5QpJfobGapMuv6Vm3/5atO3OWAp5av/XNSBbCR14C/1ifeKH4j3bcOdElj9AAAAJXRFWHRkYXRlOmNyZWF0ZQAyMDI1LTAxLTAyVDE3OjQ4OjUwKzAwOjAwHMXuNQAAACV0RVh0ZGF0ZTptb2RpZnkAMjAyNS0wMS0wMlQxNzo0ODoyNCswMDowMJMSe4MAAAAodEVYdGRhdGU6dGltZXN0YW1wADIwMjUtMDEtMDJUMTc6NTM6MzgrMDA6MDD2YxSBAAAAFXRFWHRleGlmOkNvbG9yU3BhY2UANjU1MzUzewBuAAAAIHRFWHRleGlmOkNvbXBvbmVudHNDb25maWd1cmF0aW9uAC4uLmryoWQAAAAUdEVYdGV4aWY6RXhpZk9mZnNldAAxNjIwuJfX8QAAABV0RVh0ZXhpZjpFeGlmVmVyc2lvbgAwMjEwuHZWeAAAABl0RVh0ZXhpZjpGbGFzaFBpeFZlcnNpb24AMDEwMBLUKKwAAAAOdEVYdGV4aWY6TWFrZQBmbHV4EJmXhAAAABl0RVh0ZXhpZjpQaXhlbFhEaW1lbnNpb24AMTAyNPLFVh8AAAAZdEVYdGV4aWY6UGl4ZWxZRGltZW5zaW9uADEwMjRLPo33AAAAFnRFWHRleGlmOlVzZXJDb21tZW50AEFTQ0lJtONa3gAAABd0RVh0ZXhpZjpZQ2JDclBvc2l0aW9uaW5nADGsD4BjAAAAAElFTkSuQmCC

When the Teleport Web UI displays this app, it will show the contents of the custom logo:

Dashboard application with custom icon

How it works

The script loops over any img elements that have src=/web/app/application.svg. It looks for the div that lists the labels for that resource. Fortunately, the entire label is present in the DOM even if it isn't fully displayed/expanded.

Caveats

If you create Teleport app definitions via Kubernetes, you'll have a hard time due to Kubernetes' additional restrictions on the label field. They have a maximum of 63 characters, and a fairly strict regex that it must pass. Most URLs and many base64 encoded URLs will be too long to fit into the label on the Kubernetes service object. This means it isn't really practical for use with Kubernetes service -> Teleport app discovery.

This may break in the future if the Teleport Web UI changes. If Teleport has an icon other than applications.svg (like with grafana), this script will not override it. It works with both Teleport 16.x and 17.x in my quick tests.

It only works when the labels are rendered in the DOM. In list mode, labels are collapsed by default.

// ==UserScript==
// @name Custom Teleport App Access Icons
// @namespace http://gist.github.com/programmerq
// @version 1.5.1
// @description Adds the ability to set a custom icon. Base64 encode a 72x72 png and assign it to a label called `teleport.dev/icon`. Alternatively, you can set a `teleport.dev/icon-url` label with the URL to use as the `src`. You can do a bare URL or base64 encode it. Note that using app discovery in kubernetes means that you are subjected to the more stringent requirements in kube labels. They are limited to 63 characters total, and many special characters in a URL are not allowed.
// @author Jeff Anderson
// @match https://*.teleport.sh/*
// @downloadURL https://gist.githubusercontent.com/programmerq/6a6c127a94c9018f1a0503fa0140770a/raw/custom-icon.js
// @updateURL https://gist.githubusercontent.com/programmerq/6a6c127a94c9018f1a0503fa0140770a/raw/custom-icon.js
// @icon https://raw.githubusercontent.com/gravitational/teleport/refs/heads/master/web/packages/design/src/ResourceIcon/assets/application.svg
// @grant none
// ==/UserScript==
(function () {
'use strict';
// Helper function to decode Base64
function decodeBase64(encoded) {
try {
return atob(encoded);
} catch (e) {
console.error("Failed to decode Base64:", encoded, e);
return null;
}
}
// Function to replace the src of the img with Base64 or decoded URL
function replaceAppIcons() {
// Find all matching img elements
const imgs = document.evaluate(
"//img[@src='/web/app/application.svg?no-inline']",
document,
null,
XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,
null
);
// Loop through each matching img
for (let i = 0; i < imgs.snapshotLength; i++) {
const img = imgs.snapshotItem(i);
// Find the div containing labels (adjust XPath if necessary)
const labelsDiv = document.evaluate(
"./following-sibling::div[1]/div[3]/div | ./following-sibling::div[6]",
img,
null,
XPathResult.FIRST_ORDERED_NODE_TYPE,
null
).singleNodeValue;
if (labelsDiv) {
// Look for the 'teleport.dev/icon' label (Base64)
const iconLabel = Array.from(labelsDiv.querySelectorAll("div")).find(div =>
div.textContent.startsWith("teleport.dev/icon:")
);
// Look for the 'teleport.dev/icon-url' label (Base64 encoded URL)
const iconUrlLabel = Array.from(labelsDiv.querySelectorAll("div")).find(div =>
div.textContent.startsWith("teleport.dev/icon-url:")
);
// Use Base64 if available; fallback to decoded URL
if (iconLabel) {
const base64Value = iconLabel.textContent.split(":")[1]?.trim();
if (base64Value) {
img.src = `data:image/png;base64,${base64Value}`;
console.log(`Replaced img src with base64: ${base64Value}`);
iconLabel.setAttribute("style", "display: none")
}
} else if (iconUrlLabel) {
const encodedUrl = iconUrlLabel.textContent.split(":")[1]?.trim();
const decodedUrl = decodeBase64(encodedUrl);
if (decodedUrl) {
img.src = decodedUrl;
console.log(`Replaced img src with decoded URL: ${decodedUrl}`);
iconUrlLabel.setAttribute("style", "display: none")
}
}
}
}
}
// Run the replacement on page load and when the DOM changes
const observer = new MutationObserver(replaceAppIcons);
observer.observe(document.body, { childList: true, subtree: true });
// Initial run
replaceAppIcons();
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment