analytics icon indicating copy to clipboard operation
analytics copied to clipboard

Clarify Required Length of SECRET_KEY_BASE

Open m-mattia-m opened this issue 4 months ago • 3 comments

Past Issues Searched

  • [x] I have searched open and closed issues to make sure that the bug has not yet been reported

Issue is a Bug Report

  • [x] This is a bug report and not a feature request, nor asking for self-hosted support

Using official Plausible Cloud hosting or self-hosting?

Self-hosting

Describe the bug

While setting up Plausible on my Kubernetes cluster, I configured SECRET_KEY_BASE to 32/48 bytes, as several parts of the documentation mention that it should be 32/48 bytes. However, the database migration failed, and I encountered a flood of logs related to missing tables.

After a lot of debugging, I eventually discovered that the real issue wasn’t with the database, but with the length of SECRET_KEY_BASE. Buried in the logs, I found this message:

secret_key_base to be at least 64 bytes

It would be super helpful if this requirement was clearly mentioned in the official setup guide and other relevant notes throughout the repository.

Some locations where this clarification would be great to add:

Expected behavior

The setup guide and documentation should reflect the actual requirement for SECRET_KEY_BASE to be at least 64 bytes, to help others avoid this confusion.

Thanks for your great work on Plausible – looking forward to seeing this small clarification improve the onboarding experience!

Screenshots

No response

Environment

- OS: MacOS
- Browser: Arc
- Browser Version: Version 1.106.0 (66192); Chromium Engine Version 138.0.7204.185

m-mattia-m avatar Aug 04 '25 21:08 m-mattia-m

👋 @m-mattia-m

Would you be able to share the logs and more info on your Kubernetes setup? I think the problem might be coming from elsewhere. Plausible CE still works with base64-encoded random 48 bytes SECRET_KEY_BASEs -- I checked by re-attempting the quickstart from https://github.com/plausible/community-edition/tree/v3.0.1

ruslandoga avatar Aug 19 '25 01:08 ruslandoga

Unfortunately, I don’t have the configuration anymore, but here are the logs I saw at the time:

...
19:07:07.495 [error] Error refreshing 'sites_by_domain' - %Postgrex.Error{message: nil, postgres: %{code: :undefined_table, line: "1449", message: "relation \"teams\" does not exist", position: "687", file: "parse_relation.c", unknown: "ERROR", severity: "ERROR", pg_code: "42P01", routine: "parserOpenTable"}, connection_id: 255662, query: "SELECT s0.\"id\", s0.\"domain\", s0.\"domain_changed_from\", s0.\"ingest_rate_limit_scale_seconds\", s0.\"ingest_rate_limit_threshold\", t2.\"id\", t2.\"identifier\", t2.\"name\", t2.\"trial_expiry_date\", t2.\"accept_traffic_until\", t2.\"allow_next_upgrade_override\", t2.\"locked\", t2.\"setup_complete\", t2.\"setup_at\", t2.\"hourly_api_request_limit\", t2.\"notes\", t2.\"grace_period\", t2.\"inserted_at\", t2.\"updated_at\", g1.\"id\", g1.\"event_name\", g1.\"page_path\", g1.\"scroll_threshold\", g1.\"display_name\", g1.\"site_id\", g1.\"inserted_at\", g1.\"updated_at\", s0.\"domain\", s0.\"domain_changed_from\" FROM \"sites\" AS s0 LEFT OUTER JOIN \"goals\" AS g1 ON (g1.\"site_id\" = s0.\"id\") AND NOT (g1.\"currency\" IS NULL) INNER JOIN \"teams\" AS t2 ON t2.\"id\" = s0.\"team_id\" WHERE (s0.\"updated_at\" > $1::timestamp + ($2::numeric * interval '1 minute')) ORDER BY s0.\"updated_at\""}
19:07:14.853 [error] #PID<0.5372.0> running PlausibleWeb.Endpoint (connection #PID<0.5370.0>, stream id 2) terminated
Server: plausible.domain.tld:80 (http)
Request: GET /register
** (exit) an exception was raised:
    ** (ArgumentError) cookie store expects conn.secret_key_base to be at least 64 bytes
        (plug 1.16.1) lib/plug/session/cookie.ex:184: Plug.Session.COOKIE.validate_secret_key_base/1
        (plug 1.16.1) lib/plug/session/cookie.ex:176: Plug.Session.COOKIE.derive/3
        (plug 1.16.1) lib/plug/session/cookie.ex:95: Plug.Session.COOKIE.put/4
        (plug 1.16.1) lib/plug/session.ex:96: anonymous fn/3 in Plug.Session.before_send/2
        (elixir 1.18.3) lib/enum.ex:2546: Enum."-reduce/3-lists^foldl/2-0-"/3
        (plug 1.16.1) lib/plug/conn.ex:1850: Plug.Conn.run_before_send/2
        (plug 1.16.1) lib/plug/conn.ex:441: Plug.Conn.send_resp/1
        (phoenix 1.7.20) lib/phoenix/router.ex:484: Phoenix.Router.__call__/5
19:07:22.498 [error] Error refreshing 'hostname_allowlist_by_domain' - %Postgrex.Error{message: nil, postgres: %{code: :undefined_table, line: "1449", message: "relation \"shield_rules_hostname\" does not exist", position: "96", file: "parse_relation.c", unknown: "ERROR", severity: "ERROR", pg_code: "42P01", routine: "parserOpenTable"}, connection_id: 255667, query: "SELECT s0.\"id\", s0.\"hostname_pattern\", s0.\"action\", s1.\"domain\", s1.\"domain_changed_from\" FROM \"shield_rules_hostname\" AS s0 INNER JOIN \"sites\" AS s1 ON s1.\"id\" = s0.\"site_id\" WHERE (s0.\"updated_at\" > $1::timestamp + ($2::numeric * interval '1 minute')) ORDER BY s0.\"updated_at\""}
...

m-mattia-m avatar Aug 19 '25 20:08 m-mattia-m

I guess it kind of worked out for those using the provided openssl command since it generates a 64 byte string (base64 encoded 48 random bytes).

$ openssl rand -base64 48
2bmoLUKu7wqkuqo4JgQf3nYJ/s0QuEasszwEMTCrndfea4e9gBoM7DUNaBDJGmQw
iex(1)> byte_size "2bmoLUKu7wqkuqo4JgQf3nYJ/s0QuEasszwEMTCrndfea4e9gBoM7DUNaBDJGmQw"
64

But I see how it would be useful to be clear about it in the requirements!

ruslandoga avatar Aug 20 '25 04:08 ruslandoga