Skip to main content

Changing Passwords · API Keys

PlantPulse AI uses a graph storage (Neo4j) account and a handful of API keys that connect internal services. Since 2026.09, the internal API keys are freshly generated by the installer with each install (only when they're placeholders). Starting in 2026.10, the two stored-credential encryption keys (PI_COPILOT_SECRET_KEY · PI_DT_SECRET_KEY) are also generated together at first install. The Neo4j password is not auto-generated, so you must fill it in during installation (if a placeholder remains, START=1 will refuse to start). After that, follow this document whenever a leak is suspected or personnel change.

There is a single tool for changing values: bin/passwd.sh. The three products — platform, ai, studio — each have it with the same name and the same usage.

There is exactly one authoritative configuration: /etc/kopens/plantpulse-ai.env

This product's secrets live in /etc/kopens/plantpulse-ai.env. It is outside the repo tree, not inside the installation directory, and its permissions are 0600.

🔴 Do not edit compose/.env. Nobody reads it — the stack won't change.

The file that passwd.sh writes and the file that the stack-launching script reads are one and the same. This is why rotated values take effect as expected.


This is different from application accounts​

Don't mix the two up.

WhatWhere to change it
Application accountsAccounts people log in withManaged from each app's admin screen
Infrastructure · API keysDB accounts, inter-service auth keysThis document — bin/passwd.sh

This document covers only the latter, infrastructure credentials.


What can be changed​

Start by viewing the list. Running it with no arguments does the same thing.

cd /opt/kopens/plantpulse-ai-docker
bin/passwd.sh --list

It shows the key · ID · current value (first 3 characters only) · target · and where that value lives. If you need to see the full value, use --list --show.

KeyWhatHow it changes
PI_NEO4J_PASSWORDNeo4j — LightRAG knowledge graph and factory graph mirror, both instancesThe server account is authoritative — run ALTER USER (cypher-shell) on both instances
PI_RAG_API_KEYRAG API key — shared by copliot-web · lightrag · rag-apiFile
PI_VLLM_API_KEYvLLM API key — shared by lightrag · insight-apiFile
PI_LLM_API_KEYLiteLLM master keyFile
PI_LITELLM_SALT_KEYLiteLLM salt key — 🔴 do not rotate (see below)File
PI_INBOUND_API_KEYBackend inbound key — received by rag · insight · parser · vision · digital thread (if empty, those five refuse to start), and called with the same value by copilot · admin console backendsFile

Key names are exactly the environment variable names. There are no aliases like neo4j or rag — if aliases existed, you'd have to maintain a separate table mapping names to variables, and that table would inevitably drift from reality.

All six start with PI_

The names were cleaned up once (2026-08-27). There is no fallback, so passing an old name results in rejection with unknown key — treated exactly like a typo (if a similar name exists, it will be suggested).

Old nameCurrent name
DOCKER_NEO4J_PASSWORDPI_NEO4J_PASSWORD
RAG_API_KEYPI_RAG_API_KEY
VLLM_API_KEYPI_VLLM_API_KEY
LITELLM_MASTER_KEYPI_LLM_API_KEY
LITELLM_SALT_KEYPI_LITELLM_SALT_KEY
PP_INBOUND_API_KEYPI_INBOUND_API_KEY

Inside containers, each service may see a different name (CO_INBOUND_API_KEY · TS_INBOUND_API_KEY …), but only these six values are the ones operators actually touch. compose splits a single value into multiple names.

Inbound key — empty value refuses startup, placeholder passes

Leaving it blank is blocked two ways. The pre-start check (bin/lint-env.sh) will not bring up the stack with PI_INBOUND_API_KEY is empty …, and the backend services (rag · insight · parser · vision · digital thread) also refuse to start if their own key is empty (previously these would pass through without authentication). The only way to bring it up without authentication is to deliberately set the dev-only switch PI_INBOUND_AUTH_DISABLED=true, and if a key is present, that switch is ignored → Environment Variable Configuration

However, the CHANGE_ME_… placeholder is not an empty value and passes that check, and this string is identical across all installs. When changing it, make sure to set a new value (--generate recommended), and verify with --list that it took effect. passwd.sh brings the entire stack back up after editing the file, so both the side receiving this key and the side calling it are aligned to the new value at once.

PP_* is not here

PP_* like PP_PG_PASSWORD · PP_CASSANDRA_PASSWORD · PP_REDIS_PASSWORD are platform credentials and cannot be changed from this product (you don't have permission to change the server side). They are not in the list, and passing them is rejected just like a typo. The prefix tells you the owner — PP_ = platform, PI_ = AI.

Starting in 2026.10, containers receive the platform account · password from the platform at startup → Environment Variable Configuration. So after changing it via bin/passwd.sh on the platform side, simply restarting the relevant container on the AI side picks up the new value (bin/restart.sh). Host scripts that run pre-start DB provisioning etc. also fetch from the platform on every run, so there's nothing to fix on the AI side — the host env has no lines for the platform account · password. If an old install still has lines for PP_PG_* · PP_CASSANDRA_* · PP_REDIS_PASSWORD, they are not read, and the pre-start check will tell you to remove them.

The platform secrets that remain in the host env are two: PP_DATALAKE_ADMIN_API_KEY (the key used to receive credentials) and PP_API_TOKEN (platform V5 API). If these values change on the platform side, update that line in /etc/kopens/plantpulse-ai.env by hand and recreate with bin/restart.sh.

Human login accounts are not here either

The admin console (PI_ADMIN_USER · PI_ADMIN_PASSWORD) and the LiteLLM admin screen (PI_LITELLM_UI_USERNAME · PI_LITELLM_UI_PASSWORD) are not in the passwd.sh list. Edit the environment file and recreate with bin/up.sh → Changing the Initial Password


Changing values​

Omitting the value and entering it at the prompt is the recommended path. The password won't be left in ps output or shell history. It's requested twice to confirm the values match.

bin/passwd.sh PI_INBOUND_API_KEY

You can also pass the value directly as an argument.

bin/passwd.sh PI_RAG_API_KEY=<new-value>

Passing several at once bundles the restart into a single occurrence. Use this form during commissioning.

bin/passwd.sh PI_VLLM_API_KEY=<new-value> PI_LLM_API_KEY=<new-value>

Let the tool generate the value — --generate​

For keys where a human doesn't need to choose the value, it's safest to let the tool generate it. Since nothing is typed, it won't end up in shell history or ps, and it's generated with the same rules (length · series prefix) as the installer.

bin/passwd.sh --generate PI_INBOUND_API_KEY PI_RAG_API_KEY

The generated value tells you which series it belongs to from its leading characters. Even if only a fragment is visible in logs or a settings screen, you can tell what it is, and pasting it into the wrong slot becomes noticeable. The prefix isn't secret, and it doesn't shorten the length of the random portion that follows.

KeyPrefix
PI_INBOUND_API_KEYppin_
PI_VLLM_API_KEYppvl_
PI_RAG_API_KEYpprg_
PI_LLM_API_KEYsk-pplm_
PI_ADMIN_SESSION_SECRETppss_ (generated by the installer only — not in the passwd.sh list)

Some keys are refused by --generate.

  • PI_LITELLM_SALT_KEY — not a key but a salt (see below)
  • Keys with no generation rule, such as PI_NEO4J_PASSWORD — supply the value directly or enter it at the prompt
  • PI_ADMIN_PASSWORD · PP_* — a human must log in with this value, or it must match a value set by the platform
  • PI_COPILOT_SECRET_KEY · PI_DT_SECRET_KEY — not in the list. A random swap would be equivalent to discarding stored values (see "Stored credential encryption keys" below)

To see what would change without touching anything, use --dry-run.

bin/passwd.sh --dry-run PI_RAG_API_KEY=<new-value>

Running it first shows what would change and the impact, then asks for confirmation via [y/N]. To skip confirmation in automation, use --yes.

You cannot omit the value in non-interactive mode

In environments without a terminal, such as scripts or cron, omitting the value means no prompt can be shown, and it ends in an error. In that case, pass it in KEY=<value> form.

Rotating PI_LITELLM_SALT_KEY breaks things «silently»

This value is the key that encrypts external provider API keys — entered on the admin console's Model Registration screen — into the gateway DB. Rotating it does not re-encrypt what's already stored — the container comes up healthy and the model list looks unchanged, but calls to those models alone start failing. passwd.sh shows a warning before confirmation if you try to change this key. Decide it once at install time, and include it in your backup target.


What happens when you run it​

The order is fixed.

OrderWhat it does
1Start from the server account that is authoritative — for PI_NEO4J_PASSWORD, change the account on both Neo4j instances first. Before changing, it confirms both can log in with the current value, and if either fails, it stops without changing anything
2/etc/kopens/plantpulse-ai.env is updated with the new value (permissions 0600 preserved)
3docker compose up -d --wait — recreates services so the new value takes effect (invoked by explicitly specifying the compose file and env file, so it works the same regardless of which directory you run it from)

The server side is changed first, then the file is aligned. If done in reverse, the server would still have the old value while the file has the new one, leaving authentication broken until the restart.

The AI path is interrupted during this. Perform it during a maintenance window.

If it fails midway​

WhereStateWhat to do
Step 1 (Neo4j account change)Stops without touching the file. If the first instance changed but the second failed, it prints the name of the already-changed instanceCheck with bin/status.sh whether plantpulse-neo4j · plantpulse-digital-thread-neo4j are up. If only one side changed, revert that side to the old value (ALTER CURRENT USER) and run again
Step 3 (restart)The file already has the new valueCheck status with bin/status.sh and address the cause of the startup failure

Stored credential encryption keys — replace with _PREVIOUS​

PI_COPILOT_SECRET_KEY (copilot) · PI_DT_SECRET_KEY (digital thread connector registry) are the keys that seal credentials entered on-screen (external connector auth values · LLM keys · SMTP passwords · Slack webhooks · Web Push private keys, connector credentials) into the DB. Not in the passwd.sh list — simply supplying a new value would make it impossible to unseal what was sealed with the old value. Rotation is done via a procedure that supplies the old value alongside so it can be «re-sealed».

OrderWhat it does
1Place the old value (comma-separated for multiple) in PI_COPILOT_SECRET_KEY_PREVIOUS, and the new value (openssl rand -base64 32) in PI_COPILOT_SECRET_KEY
2bin/restart.sh plantpulse-copilot-web — on startup, stored values are re-sealed with the new key. Check the count via [SECRETS] sealed at rest … in the logs
3Clear PI_COPILOT_SECRET_KEY_PREVIOUS and recreate once more

PI_DT_SECRET_KEY is the same — place the old value in PI_DT_SECRET_KEY_PREVIOUS, recreate plantpulse-digital-thread-api, then clear it.

Rotating without the old value prevents copilot from starting

If the key changed but the old value is missing, copilot refuses to start with Stored secrets cannot be decrypted, logging the sealed rows and the SQL to clear them — it does not silently call without authentication. If the key is lost, the only option is to re-enter that credential on-screen. Make sure your env file backups include these two values.


Verification​

bin/passwd.sh --list

If the first 3 characters of the current value changed, it has taken effect. The first 3 characters are shown so you can see at a glance whether the default value remains unchanged.