"401: Unauthorized" iframe CARD of Grafana not working

Cheers. Do you embed internal address or external one with https?

I can confirm that after I changed the port to 3000, the iframe panel showed up on may app.

When I connected to HA using same IP equals IP in URL in iframe, frame work properly/
But when I connected using duckdns.org host or using host from my hosts file, frame show error 401.
None of the above recipes helped.

Unfortunately, this problems still happens on my android mobile phone even change that port to 3000.

But on PC/Browser, it works always fine. Very strange.

1 Like

Same here. Android App with Tablet user isn’t working. Android App with my personal User on Smartphone works perfectly fine, also with my personal User on the PC browser.

Would be interesting to get this resolved for all devices.

1 Like

Hi, I have posted alternative solution for iframe with Grafana.
If you are running Home Assistant and Grafana on a reverse proxy and SSL enveronment, this might be useful.

Enabling anonymous login fixes this issue with setup like below but creates security problems. One way to fix that from HA side is to introduce sending additional headers for lovelace iframe card.
Grafana api keys can be used like this:

curl -H “Authorization: Bearer eyJrIjoiUnhKa2swM2FrR0k4eUhyUHQ5dDVoVzN3U2cwVdfgfdrjht554iaWQiOjF9” https://grafana.mysetup.org/api/hassio_ingress/nZYGqQ-X7eLkUZpNFdRU47lv_/api/dashboards/home

Then grafana api keys can be used but this also creates other security issues since the api key will be visible for everyone who can access the lovelace dashboard. Though it is a less severe issue than completely exposing grafana with anonymous login since if someone unwanted can access your dashboard, you will have other problems.
This can also be mitigated by using secrets in the lovelace ifrane card to some degree too.

    value: 'true'
    value: 'true'
    value: Org
    value: Viewer

There is an open feature request for this:

For me, no advice worked. I was able to display grafana panels from external adresses, but not now anymore. I only get jumping grafana logo.

I have the same issue.
I get an 401: Unauthorized error on a first opening lovelace page.
I have to open a grafana panel first and then I have to refresh a lovelace page


Same 401 issue… 2022.8.3.
I saw many different solutions out there, but… I’m a bit confused: after years of HA-Grafana integration… isn’t there, yet, a straight direction to solve an issue that ANYONE using Grafana in HA would have? I mean… anyone using Grafana - I think - would then add his graphs within the other dashboards… no?

I also have this issue and I’m sad there’s no nice solution.
Right now I’m just manually opening Grafana (so that I’m somehow authenticated) and after that it works … (for 1 day).

What is the adress you are using in the card ?
Mine is like this https://externalIP:3000/
you need to redirect the port to your server.
If you’re using SSL, don’t forget to indicate the adress of it. If you use NPM it is not / but /nginxproxymanager/live/npm-5/privkey.pem

Nginxproxymanager. Graphana uses X-Frame-Options for cross site protection against rendering in an iFrame

In NPM on your config add a custom location for / and add the below config

proxy_hide_header X-Frame-Options

alright check it out, its a dirty hack but this how i got around this problem:

  1. create a folder called www in your homeassistant config folder, so its /homeassistant/config/www
    1a. if the folder dint exist, make sure you restart home assistant!!!
  2. create a new html file in the new www directory, anyfilename.html
    2a. the contents of anyfilename.html are:
<!DOCTYPE html>
   <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
   <meta http-equiv="Pragma" content="no-cache">
   <meta http-equiv="Expires" content="0">
  <p id = "result"></p>
     function redirect () {
        setTimeout(myURL, 2000);
        var result = document.getElementById("result");
        result.innerHTML = ".";

     function myURL() {
        const queryString = window.location.search;
        document.location.href = `/api/hassio_ingress/HASSIOingresRandomURL/${queryString}`;

change HASSIOingresRandomURL to what your ingress url isbroken 401 webpage card has… example would be /api/hassio_ingress/whatevertokenRandomURL/d-solo/7Tl95WoGz/server-stats Also take note of the 2000 milosecond (2s) load delay.

  1. In your dashboard that contains a webpage (iframe) card that has a broken 401 /api/hassio_ingress/HASSIOingresRandomURL/ Change it to /local/anyfilename.html with arguments at the end. Example would be: /local/anyfilename.html?orgId=1&refresh=1m&from=now-1h&to=now&panelId=3

  2. we will add a dummy webpage card anywhere in the dashboard tab with the following settings:

- type: iframe
  url: /TheSideBarURL
  aspect_ratio: 0%

This creates an invisible card to force load grafana as if you clicked on it on the sidebar.

  1. Now when you load the dashboard tab, grafana is loaded by the invisible webpage card and after 2 seconds your HASSIOingresRandomURL with arguments & w/e you want are loaded in.

Until home assistant fixes passing url parameters & paths to ingressURL’s on the sidebar, we have to come up with crap like this… Tested working on external URLs like CASA & Cloudflared. Anyone not logged in who accesses /local/anyfilename.html are still greeted with 401 not authorized.

You can test this faster by using desktop browser. Closing the browser completely (not just the tab) will force 401 to appear.

Lifesaver! For those banging their heads, the config line should be followed by a semicolon, like so:

proxy_hide_header X-Frame-Options;

Actually a better way if you have a fqdn for both Graphana and Home assistant. Remove my old command and add this instead. It’s more secure, and will only allow iframe renders from the named ancestors. Vs completely disabling and allowing it to be used in an iframe anywhere.

add_header Content-Security-Policy "frame-ancestors graphana.yourdomain.com homeassistant.yourdomain.com;";
1 Like

I fixed this in Scrypted by having the add-on write a html card to config/www. The contents of that card will use an access token to create an ingress session using the web socket api, and then navigate to the ingress url after setting the ingress_session cookie.

In my example, the card is written to /config/www/scrypted_card.html and used within the Webpage frame as /local/scrypted_card.html (with access token in query string).

The access token can be found in localStorage on desktop browsers, but seemingly is unavailable in mobile apps. Thus, it’s best to provide the token in a query string.


        const embedUrl = new URL(window.location.href);
        const cameraId = embedUrl.searchParams.get('id');

        let accessToken = embedUrl.searchParams.get('accessToken');
        if (!accessToken) {
            const hassTokens = JSON.parse(localStorage.hassTokens)
            accessToken = hassTokens.access_token;

        const u = new URL('/api/websocket', window.location.href);
        const https = u.protocol === 'https';
        u.protocol = https ? 'wss' : 'ws';
        const ws = new WebSocket(u.toString());

        const wsSend = data => {

        ws.addEventListener('open', () => {
                type: 'auth',
                access_token: accessToken,

                type: "supervisor/api",
                endpoint: "/addons",
                method: "get",
                id: 1,


        let ingress_url;
        ws.addEventListener('message', e => {
            const data = JSON.parse(e.data);
            if (data.id === 1) {
                const scrypted = data.result.addons.find(addon => addon.slug.endsWith('_scrypted'));
                    type: "supervisor/api",
                    endpoint: `/addons/${scrypted.slug}/info`,
                    method: "get",
                    id: 2,
            else if (data.id === 2) {
                ingress_url = data.result.ingress_url;
                    type: "supervisor/api",
                    endpoint: "/ingress/session",
                    method: "post",
                    id: 3,
            else if (data.id === 3) {
                const session = data?.result?.session;
                document.cookie = `ingress_session=${session}; path=/`;
                window.location.href = `${ingress_url}endpoint/@scrypted/nvr/public/#/iframe/${cameraId}`;
            else {



Just wanted to let everyone know, that I struggled with the same issues, integrating Grafana in Lovelace.

My root need was to have a nice graph, updated regularly and with many configuration option.

So after using ChatGPT, I came to the solution of using " Lovelace Mini Graph Card" in HACS.

It may not be as powerful or beautiful as Grafana, but for a Lovelace card, it’s pretty amazing!

Here’s the link to this doc for this card: https://github.com/kalkih/mini-graph-card/blob/master/README.md#example-usage


1 Like

ChatGPT is pretty awesome for coding! It even knows some node-red coding too. It can create and adapt flows and gives examples to paste in. Tho it does need a lot of correcting at times, it gives you a good base to start from!

Will take a look your link. It sounds great!

This solution work for me inside local net, outside local (behind the NAT on router) and any another start URL on HA app or browser.

Check it.

  1. Add vars in Addons-Grafana-Configuration tab:
    value: anonymous
    value: Hassio
    value: 'true'
    value: 'true'
    value: none
    value: 'true'
    value: '365'
    value: 'true'
  1. No need change port for Grafana. Restart Grafana addon.
  2. Change URL on your HA cards:
  • delete address “http://IPAddress:8123” from Grafana panels urls.
  • new links should look like “/api/hassio_ingress/…”
  1. Clear cache browser or delete data in HA app.
  2. Login in HA and enter in Crafana web first. After that all panels starts loading.

Hope this help

1 Like