Production Hardening
Sovereign starts with no secrets configured: it generates the ones it needs on first run, so a first run works with nothing but a media path. Every one of those values is then written to your data volume and read back on each start, so a restart changes nothing.
Whether you pin them yourself depends on how you deploy. You need to when the data volume doesn’t persist, or when more than one instance has to agree on them.
What Sovereign Generates For You
Section titled “What Sovereign Generates For You”| Value | When you leave it unset | Where it’s kept |
|---|---|---|
| Obfuscation alphabet | A random shuffle is generated on first run | server.json on your data volume |
| Asset signing key | A random 64-character key is generated on first run | server.json on your data volume |
| Client ID | A random GUID is generated on first run | server.json on your data volume |
| Device client ID | A random GUID is generated on first run | server.json on your data volume |
| Token certificates | A self-signed pair is generated on first run | The certificates folder on your data volume |
| Admin credentials | A random username and password are seeded on first run | The database |
server.json holds the first four together because they share one lifecycle: generated once, then kept. It’s readable only by the user Sovereign runs as, because it holds the asset signing key. There’s no setting for the device client ID, which is the one televisions pair under. Each install generates its own, and the TV reads it from the server when it starts up.
None of this needs your attention on a single instance with storage that sticks around. It needs it the moment either of these is true:
- Your data volume is disposable. A container that starts with an empty volume regenerates everything, and the table below is what breaks each time it does.
- You run more than one instance. Replicas each generate their own values, so which one answers a request decides whether it works.
What Changing One Costs
Section titled “What Changing One Costs”Whether a value changes because you rotated it deliberately or because the volume it lived on went away, the effect is the same:
| Value | What breaks | How it resolves |
|---|---|---|
| Obfuscation alphabet | Saved and shared links stop resolving, and a client holding an old ID gets a not-found | Users navigate to the title again |
| Asset signing key | Outstanding media URLs are rejected, so playback and images fail mid-session | A page refresh fetches freshly signed URLs |
| Client ID | Every session is invalidated | Users sign in again |
| Device client ID | Every paired television is signed out | Pair each television again |
| Token certificates | Issued tokens can no longer be verified, so signed-in users are rejected on their next request | Users sign in again |
| Admin credentials | Nothing: the variables stop applying once the account exists | Change the password in the interface instead |
None of these are destructive, and none of them touch your library, your watch history, or your users.
Generate Your Values
Section titled “Generate Your Values”Use the generator below to produce the alphabet, signing key, client ID and both certificates at once, then paste the results into your secret store.
Secret Generator
Generated in your browser with the Web Crypto API. Nothing you generate is transmitted or stored: this documentation site is static and has no backend to send it to. The values live on this page only, and reloading discards them.
Stable values
Alphabet, asset signing key, and client ID.
Token certificates
A signing and an encryption certificate, each a 2048-bit RSA key valid for five years, packaged as a password-protected PKCS#12 archive.
If you prefer your own tooling, these produce equivalent values:
# Obfuscation alphabet (a shuffle of the same 62 characters)echo 0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ \ | fold -w1 | shuf | tr -d '\n'
# Asset signing keyLC_ALL=C tr -dc 'A-Za-z0-9' < /dev/urandom | head -c 64
# Client IDuuidgenToken certificates need a little more than one command; the generator produces them, and Generating Self-Signed Certificates covers the OpenSSL equivalent.
The Obfuscation Alphabet
Section titled “The Obfuscation Alphabet”The alphabet in Sovereign__Obfuscation__Alphabet decides how the IDs in Sovereign’s links and in the app are written. Change it and links saved before the change stop working.
Keep the same 62 characters and only reorder them. Each character must appear exactly once, and the set has to stay single-byte, so no accented letters, punctuation, or emoji.
Sovereign__Obfuscation__Alphabet=jI9uLMqh3npGNyKOiCT0acSJQb7oflzm5eEkvsBdgDY18Fx6wtrZ4XPVARWH2UThe alphabet isn’t a secret in the way a key is: knowing it doesn’t grant access to anything. Treat it as private anyway.
The Asset Signing Key
Section titled “The Asset Signing Key”Sovereign__Obfuscation__AssetSecuritySecretKey protects every link to a video, image or subtitle that Sovereign hands out, and a link made without it is refused. Signed Asset URLs covers what those links guarantee.
This one is a real secret, though a leaked key on its own isn’t a way in. To turn it into a working link, someone also needs to reach your server and to know the exact path of a file on it, and those paths aren’t easy to work out from outside: artwork and seek previews sit under IDs written with your install’s own alphabet, and a video’s segments only exist while someone is watching it, and for a short while after. Both the key and the alphabet live in server.json, so give the data volume the same care. If the key does leak, change it: every outstanding link stops working, and clients get fresh ones on their next refresh.
Sovereign__Obfuscation__AssetSecuritySecretKey=<64-char-secret>Use at least 32 random alphanumeric characters; the generator produces 64, which is what Sovereign generates for itself too. If you run more than one instance behind a load balancer, they all need the same value, otherwise a URL signed by one is rejected by the next.
Changing the key can mean Sovereign’s metadata service counts your server twice on the day you change it (the Privacy Policy explains that count).
The Client ID
Section titled “The Client ID”Sovereign__Authentication__ClientId identifies the web interface to Sovereign’s sign-in. Changing it signs every browser out, and each user signs in again. Pin it when the generated one has nowhere durable to live.
Televisions paired over the device flow aren’t affected by this one. They authenticate as a separate client with its own ID, so rotating the web interface’s ID doesn’t unpair them.
Sovereign__Authentication__ClientId=<client-id>Any stable opaque string works. A UUID is the conventional shape and is what the generator produces.
Token Certificates
Section titled “Token Certificates”Sovereign protects the tokens it issues with two certificates: one signs every token, and the other encrypts the ones only Sovereign reads, such as refresh tokens. Both are created on first start and written to the data volume, so a single instance with persistent storage needs nothing configured here. Unlike the three values above they go in the certificates folder rather than server.json, because a PKCS#12 archive is a file in its own right.
Supply your own when:
- Your data directory is disposable. A container that starts with an empty volume generates a new pair every time, invalidating every token it previously issued.
- You run more than one instance. Replicas that don’t share certificates can’t validate each other’s tokens, so whether a request succeeds depends on which replica answers it.
- You want to rotate them. Sovereign never replaces a certificate it generated, so rotating means supplying your own.
Each certificate goes in as a base64-encoded PKCS#12 archive with its password alongside it, and Token Certificates documents the four variables in full. The generator above produces both pairs, with a 2048-bit RSA key valid for five years.
First-Run Credentials
Section titled “First-Run Credentials”Sovereign__Authentication__AdminUsername and Sovereign__Authentication__AdminPassword seed the first administrator account. Leave them unset and a random pair is generated instead. Either way, the pair is printed once to the server’s console output (never its log files) when the account is created, and anything that collects that output keeps a copy. Set them explicitly so you know what they are, sign in, and change the password from User Settings.
Outside development, passwords have to meet the standard rules: at least six characters, with an uppercase letter, a lowercase letter, a digit, and a symbol. If the seed password falls short, the account can’t be created, so the server stops during startup and logs which rule the password missed.
The seed runs only while no administrator account exists. Once one does, these variables are ignored, so changing them later isn’t how you change your password.
Everything Else A Production Instance Needs
Section titled “Everything Else A Production Instance Needs”Beyond the generated material, a public deployment has five more things to get right.
Serve it over HTTPS and set the issuer to match. Sovereign__Authentication__Issuer has to be the public origin your users type, scheme included. The shipped default is a localhost address, which signs in only from the server’s own machine: Sovereign starts with a warning in its log, and the sign-in page on any other device says the issuer needs setting. Over plain HTTP every password and token crosses the network in the clear, so treat HTTP as acceptable only on a network you trust. Sovereign doesn’t terminate TLS itself, so put a reverse proxy in front of it and point the issuer at the proxy.
Leave the browser security headers to Sovereign. Once the issuer is an https address, Sovereign sends Strict-Transport-Security with a 30-day lifetime, so a browser that has visited once won’t fall back to plain HTTP. It covers only the host in the issuer: it leaves out includeSubDomains and preload, because the parent domain of a self-hosted server often fronts other services that may not all speak HTTPS. A server whose issuer is plain HTTP never sends it. If you want a longer lifetime or the subdomains covered, set the header at your reverse proxy and remove Sovereign’s copy there, so the browser sees one. Sovereign also sends a Content-Security-Policy describing exactly what its web app loads. Don’t add a second one at the proxy: a browser applies every policy it receives, so an extra policy can only take away something the web app needs, and that usually shows up as playback or sign-in quietly not working.
Tell Sovereign which proxies to trust. Behind a proxy, every request looks like it came from the proxy unless Sovereign__Network__TrustedProxyNetwork__0 names it. Without that, the activity log records your proxy instead of your users, and every user shares one sign-in rate limit. Sovereign logs a warning naming the proxy’s address the first time it sees this, and Trusted Proxy Networks walks through choosing the value.
Leave Sovereign__EnableSensitiveDataLogging off. It writes database values and credentials into the log.
Leave ASPNETCORE_ENVIRONMENT unset. The container runs as Production. Setting it to Development doesn’t loosen anything on a released image, because the development-only behaviour isn’t built into it; the server just logs a warning saying so.
Storing The Values
Section titled “Storing The Values”Pinned secrets belong wherever the rest of your secrets live: an env file kept out of version control, or a Kubernetes Secret injected as environment variables. Sovereign doesn’t read secrets mounted as files, so a Compose secrets: block won’t reach it. The Configuration Overview has the per-platform detail.
Two habits are worth picking up with this set. Record them somewhere durable before the first deploy, because a certificate archive that exists only inside a running container is one docker compose down -v away from gone. And keep them off the command line: values passed with -e land in your shell history. An env file avoids that, though anyone who can run docker inspect on the host can still read them.
A Complete Example
Section titled “A Complete Example”A sovereign.env holding every pinned value, alongside the settings that make the deployment public:
# Stable identity and asset materialSovereign__Obfuscation__Alphabet=<62-char-alphabet>Sovereign__Obfuscation__AssetSecuritySecretKey=<64-char-secret>Sovereign__Authentication__ClientId=<uuid>
# Token certificates (base64-encoded PKCS#12 archives)Sovereign__Authentication__Certificates__SigningCertificate=<base64-p12>Sovereign__Authentication__Certificates__SigningCertificatePassword=<archive-password>Sovereign__Authentication__Certificates__EncryptionCertificate=<base64-p12>Sovereign__Authentication__Certificates__EncryptionCertificatePassword=<archive-password>
# First administratorSovereign__Authentication__AdminUsername=adminSovereign__Authentication__AdminPassword=<a-strong-password>
# Public origin and proxySovereign__Authentication__Issuer=https://media.example.com/Sovereign__Network__TrustedProxyNetwork__0=172.16.0.0/12Rotating Later
Section titled “Rotating Later”Every pinned value can be changed, and What Changing One Costs is the price list. Rotate when you have reason to, at a quiet hour, and change one value at a time so an unexpected symptom points at a single cause.
Pinning a value that Sovereign had been generating is itself a rotation unless you copy across what it already generated. To keep the current values, read them out of server.json on the data volume first and pin those, rather than generating fresh ones.
Next Steps
Section titled “Next Steps”- Authentication & Security covers the sign-in model, social providers, and the asset-signing scheme in full.
- Configuration Overview is the complete environment-variable reference.
- Docker Compose and Kubernetes show where a secret file or
Secretresource plugs into a real deployment.
Titles, artwork, people and descriptions shown in screenshots are fictional and were created for illustration. Sovereign doesn't include or supply any media: it plays the library you bring.