How to display login-protected web pages on your digital signage

ENGINEERING |
How to display login-protected web pages on digital signage

In this article, I want to show how you, as an end-user, can authenticate your own digital signage players against your own back-end infrastructure, so that content sitting behind a login can still make it onto your screens.

It’s worth being clear about why this matters. A screen on a wall is not like a laptop in someone’s bag. It’s an unattended client, often sitting in a lobby, a shop floor or a break room, quietly displaying a dashboard that may well contain sensitive internal data. Much of the content that people most want on their screens (a Grafana dashboard, a sales board, an internal wiki page) lives behind some form of authentication. That creates a genuine tension: you want the convenience of simply pointing a screen at a URL, but you don’t want to bake long-lived human credentials into a device that anyone walking past could, in principle, tamper with. The good news is that there’s a spectrum of options, and the right one depends on what the target service supports and how much you control it. Let’s walk through them from the simplest to the most robust.

The OG: Basic Auth

Perhaps the oldest authentication method on the web is known as ‘Basic Auth’. This method is still very popular, largely because it is so widely supported. Most web servers and web frameworks support it in one shape or another, and when used over HTTPS it’s a perfectly fine authentication method. I’d argue it’s actually underrated for our purposes: because it’s a formal part of the HTTP specification, it’s completely standardized, stateless, and requires no brittle scripting to automate. A player can present its credentials on the very first request and be done. For a headless device that just needs to show a page, that simplicity is a feature, not a limitation.

Before we go any further, it’s worth clearing up a misconception that trips a lot of people up: Basic Auth is not simply “any website that has a username and password box.” It is a very specific mechanism defined in the HTTP standard, where the server responds with a WWW-Authenticate header, the browser pops up its own native little login dialog (not a page the site designed), and the credentials are then sent on every request in an Authorization header. If you’re typing into text fields that live inside the web page itself, that’s almost certainly not Basic Auth; it’s a custom login form (more on those below). In practice, true Basic Auth is fairly rare on consumer and business-facing services these days, but it’s still very common for internal tooling and services inside companies, which is exactly where a lot of dashboards destined for a screen tend to live.

Its honest limitation is granularity. In most Basic Auth deployments, the web server is configured with a limited set of credentials (frequently using a .htaccess file on Apache or an auth_basic stanza on Nginx). The drawback is that you don’t get much granularity in your access control: you’re typically handing the same shared username and password to every device, and revoking one means rotating all of them. That’s perfectly acceptable for many deployments, but provisioning genuinely unique per-device credentials with a static credential list quickly turns into a fairly involved configuration exercise.

That said, this isn’t an inherent limit of Basic Auth so much as of how it’s usually set up. Because Basic Auth only defines how the username and password reach the server, you’re free to validate them against whatever you like on the back end. Point it at an LDAP directory (via mod_authnz_ldap on Apache, or the auth_request module with an LDAP helper on Nginx) and you get exactly the granularity the static-file approach lacks: per-device accounts, centralized revocation, and group-based access, all managed in your existing directory rather than scattered across config files.

If Basic Auth is sufficient for your use case, Screenly has native support for authenticating against it. You can find more details in our post on how to login to websites using Screenly.

Modern Web authentication

In the olden days, it was fairly common for web pages to use Basic Auth. Those days are long gone. Today, most web pages use some form of custom authentication, where the user enters their credentials into text boxes on the page itself and hits a login button. Most modern web frameworks ship something like this out of the box, and for good reason: it gives the service full control over branding, password reset flows, multi-factor authentication and session management.

The catch, from an automation standpoint, is that none of that is standardized. Every login form is its own little snowflake: different field names, different flows, session cookies that expire, CSRF tokens, bot-detection, and increasingly an MFA prompt standing between you and the content. Even 1Password (which we’re huge fans of, by the way), where auto-filling credentials is one of the core pillars of the product, gets this wrong every so often, which tells you something about how hard reliably automating an arbitrary login form really is. So this isn’t a knock on custom logins; they’re the right choice for humans. They’re just the hardest case for an unattended screen.

When you need to authenticate against these kinds of assets in Screenly, you have two pragmatic options.

The first is our JavaScript Injector. This feature lets you execute JavaScript when a given page loads. It’s handy for things like dismissing a cookie-consent banner, but it can also drive an actual login, filling in the fields and clicking the button on your behalf. Because there’s no standard here, you’ll typically need to craft a little custom JavaScript for each site. To give you a head start, we’ve published a number of worked examples on our Playground on GitHub; they cover a range of use cases and double as inspiration you can adapt to your own. The honest trade-off is fragility: a scripted login is coupled to the page’s current markup, so a redesign on the far end can quietly break it.

Browser Extension

If hand-writing JavaScript sounds like more work than you’d like (or the target site is simply too dynamic to script reliably), our Browser Extension is often the better answer. Rather than scripting the flow, you install the extension in a regular browser, log into your web apps once just as you normally would (MFA and all), and then carry that authenticated session over to your Screenly players. For a large class of dashboards and internal tools sitting behind a modern login, this is the fastest and most robust path, precisely because it sidesteps the brittleness of replaying a login form on every page load.

Mutual TLS (mTLS)

Basic Auth and custom logins both ultimately rest on a shared secret: a password that can be phished, leaked, reused or shoulder-surfed off a screen. mTLS takes a fundamentally different approach, and it’s why I’d consider it the most robust option of the three when you’re in a position to use it. It’s a pillar of Zero Trust Networking, an idea that originated with BeyondCorp at Google (where mTLS replaced the VPN as the way to reach internal services) and was later popularized in the book Zero Trust Networks by Evan Gilman and Doug Barth back in 2017 (a highly recommended read).

Today, mTLS is supported across a wide set of tools, ranging from Apache and Nginx to modern reverse proxies like HAProxy and Caddy, as well as service meshes built on Istio/Envoy.

What makes mTLS more robust than the other two methods is that it uses cryptography rather than a username and password. Each device holds a unique certificate and proves, using the matching private key, that it is who it claims to be, and the server proves the same back to the client, which is the “mutual” in Mutual TLS. Leaning on the x509 Public Key Infrastructure (PKI), the server cryptographically validates the presented certificate against a Certificate Authority (CA) and can further pin identity using the certificate’s Common Name (CN), which on a Screenly device certificate is the device’s own ID. Because certificates also carry an expiry date, a valid certificate must be both cryptographically sound and current. The practical upshot is that there’s no shared password to phish or leak, revocation is handled at the PKI level rather than by rotating secrets across every device, and, crucially, the private key can be bound to hardware.

That last point is where it gets genuinely powerful. At Screenly, we use mTLS to authenticate our players to our back-end, and on hardware with a TPM (such as our Screenly Player Max) the private key is generated and stored inside the TPM’s secure enclave and never touches the disk. It can be used to authenticate but not extracted, so even someone with physical access can’t clone the device’s identity. Short of physically stealing the player, impersonating it is effectively impossible.

If you’d like to understand how this works under the hood on our side, my colleague Daniel wrote a detailed post on Using mTLS and TPM for digital signage security, covering how we combine mTLS with TPMs to secure the link between our players and our back-end. What follows is how you can apply the very same idea to your own infrastructure.

I’ll be upfront about the trade-offs, because mTLS isn’t free. It asks more of you than a password does: you need to control the back-end or a proxy/edge in front of it, and you take on a certificate lifecycle to manage. That’s real work, and it’s why mTLS has historically been seen as the “enterprise” option. But when you’re protecting sensitive internal dashboards on physically exposed screens, that up-front effort buys you a level of assurance the other two methods simply can’t match, and, as we’ll see, most of the configuration is a few lines.

One important prerequisite before any of the server-side configuration below will do anything: your Screenly players have to actually present their certificate when loading web assets, and that behaviour needs to be switched on first. My colleague recently walked through exactly how to enable it in Serve mTLS protected web assets on Screenly Player. There’s a fair amount of overlap with what follows; that post covers turning it on at the player end, whereas below I’ll concentrate on the server side. If your web server isn’t seeing a client certificate at all, this is the first thing to check.

Using Nginx

Not only is mTLS more secure than the other methods, it’s also relatively easy to configure. Say we have an Nginx server acting as a reverse proxy in front of an application. All we’d have to do is append the following snippet:

# The CA we validate incoming client certificates against
ssl_client_certificate /path/to/screenly/ca.crt;

# Complete the handshake even without a client cert, so that we can
# return a clean HTTP response rather than a TLS-level error
ssl_verify_client optional;

# Nginx only exposes the full subject DN (e.g. "CN=xyz.screenly.local")
# as $ssl_client_s_dn; there's no built-in variable for just the CN, so
# we pull it out ourselves with a map.
map $ssl_client_s_dn $ssl_client_s_dn_cn {
    default "";
    "~CN=(?<CN>[^,]+)" $CN;
}

# Reject anyone who didn't present a valid Screenly certificate
if ($ssl_client_verify != "SUCCESS") { return 403; }

# Optional: also restrict to one specific device by its certificate's CN
if ($ssl_client_s_dn_cn != "xyz.screenly.local") { return 403; }

A quick note on that optional rather than on: with ssl_verify_client on, Nginx rejects an unauthenticated client during the TLS handshake itself, before your if ever runs, so the visitor just gets an opaque connection error. Using optional lets the handshake complete and hands control to the if, which returns a clean 403 (and, as we’ll see, gives you a place to inspect the certificate). This will blanket reject every request that doesn’t present a valid client certificate from Screenly. But note what it does not do: any Screenly certificate is accepted at this point. To narrow it down to specific devices you’ll want to also match on the certificate’s Common Name (CN). Nginx doesn’t give you that field directly, only the full subject DN via $ssl_client_s_dn, so the map block above extracts it into a $ssl_client_s_dn_cn variable you can then match on in an if, the same way we already do for $ssl_client_verify. That’s a little fiddlier to express in Nginx than in Apache, but it’s entirely possible, and given that every Screenly device shares the same CA (more on that shortly), it’s the step that actually restricts access to your players.

Using Apache

The setup in Apache is very similar to Nginx. There’s a good article on DocuSign’s developer blog about how to do this.

SSLVerifyClient optional
SSLVerifyDepth 2
SSLCACertificateFile /path/to/screenly/ca.crt
[...]
<Directory "/var/www">
SSLOptions +StdEnvVars
<RequireAny>
Require expr %{SSL_CLIENT_S_DN_CN} == "xyz.screenly.local"
</RequireAny>
</Directory>

As with the Nginx example, optional rather than require lets the handshake complete so the Require expr can return a clean 403 (and inspect the certificate) instead of failing at the TLS layer. Unlike Nginx, Apache then makes it a bit easier to restrict to a particular node.

Getting the certificate for a given player is straightforward: in the Screenly dashboard, open the screen, go to Settings, choose View Certificate, and download it. Screenly also lets you download the issuing CA certificate, which is what you’ll need for edge setups that validate against a CA rather than a single leaf. It’s worth noting that on the Screenly Player Max, the underlying private key never leaves the device; the certificate is backed by the on-board TPM, so it can be used to authenticate but not extracted.

A word of caution here: every Screenly device shares the same CA. That means if you only verify that a client certificate chains to the Screenly CA, you’d be trusting any Screenly player in the world, not just your own. So unless you genuinely want that, don’t stop at CA validation; also pin to your specific device’s client certificate (or its Common Name), or add a ruleset that limits access to the exact devices you expect. On a proxy like Nginx, Apache, HAProxy or Caddy you can trust the CA and then restrict on the certificate’s Common Name (as shown in the Apache example above).

If you’d rather not touch your origin at all, you can offload the verification to the edge with something like Cloudflare Access. Note that Cloudflare validates client certificates against a Certificate Authority rather than an individual device certificate, so here you upload the Screenly CA, associate it with your hostname, and then, for the same reason as above, write an Access policy that pins a specific device by its Common Name rather than accepting any valid certificate from the CA. If the same dashboard also needs to be reachable by regular employees in a browser, the cleanest approach is to split the two audiences at the DNS and routing level so that only the signage hostname enforces mTLS.

If you’d like to set this up for your own web assets, we’ve put together a step-by-step guide in our support center: Configuring Mutual TLS (mTLS) for Web Assets. It walks through configuration for proxies like HAProxy and Caddy, as well as edge-based setups.

mTLS also pairs really well with our Tailscale support. Tailscale gives you a private, encrypted network to reach your back-end infrastructure without exposing it to the public internet, and layering mTLS on top means that even within that network, only players presenting a valid Screenly certificate can talk to your web assets. Since Tailscale can now be configured remotely across your fleet from your workspace settings, the two together make for a powerful, defense-in-depth setup.

Wrapping up

So which one should you reach for? Rather than crown a single winner, it’s more useful to match the method to your situation, because the real constraint is usually what the target service supports and how much of it you control:

  • If the asset already speaks Basic Auth (as a great many internal tools still do), use it. It’s standardized, headless-friendly, and trivial to automate. Just remember its credentials are typically shared and coarse-grained.
  • If you’re facing a custom login form you don’t control, reach for the Browser Extension first, and fall back to the JavaScript Injector when you need finer control. Both are pragmatic ways to get past a login that was designed for a human, not a screen.
  • If you control the back-end (or a proxy/edge in front of it) and the content warrants it, mTLS is the strongest option. It replaces a phishable shared secret with a hardware-backed cryptographic identity: no password to leak, and, on a TPM-equipped player, no key to steal.

And these aren’t mutually exclusive. The most resilient setups tend to layer them. For instance, putting your back-end on a private Tailscale network and enforcing mTLS on top means that reaching the service and proving your identity to it are two independent locks. Defense in depth, applied to the humble screen on the wall.

If you’d like a hand figuring out which approach fits your deployment, get in touch and we’re happy to help.

Viktor Petersson
Viktor Petersson View Profile
CEO and Co-founder of Screenly. Viktor loves taking ideas and turning them into products and services. You can frequently find him on the speaking circuit around the globe. He spent a big part of his adult life as a digital nomad. Viktor is also passionate about DevOps and IoT security. You can connect with Viktor on LinkedIn.

Recent Posts

Display your best content with Screenly digital signs.

Get started today quickly and easily with Screenly's secure, enterprise-grade digital signage.

Screenly digital signage display