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.
/etc/kopens/plantpulse-ai.envThis 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.
| What | Where to change it | |
|---|---|---|
| Application accounts | Accounts people log in with | Managed from each app's admin screen |
| Infrastructure · API keys | DB accounts, inter-service auth keys | This 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.
| Key | What | How it changes |
|---|---|---|
PI_NEO4J_PASSWORD | Neo4j — LightRAG knowledge graph and factory graph mirror, both instances | The server account is authoritative — run ALTER USER (cypher-shell) on both instances |
PI_RAG_API_KEY | RAG API key — shared by copliot-web · lightrag · rag-api | File |
PI_VLLM_API_KEY | vLLM API key — shared by lightrag · insight-api | File |
PI_LLM_API_KEY | LiteLLM master key | File |
PI_LITELLM_SALT_KEY | LiteLLM salt key — 🔴 do not rotate (see below) | File |
PI_INBOUND_API_KEY | Backend 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 backends | File |
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.
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 name | Current name |
|---|---|
DOCKER_NEO4J_PASSWORD | PI_NEO4J_PASSWORD |
RAG_API_KEY | PI_RAG_API_KEY |
VLLM_API_KEY | PI_VLLM_API_KEY |
LITELLM_MASTER_KEY | PI_LLM_API_KEY |
LITELLM_SALT_KEY | PI_LITELLM_SALT_KEY |
PP_INBOUND_API_KEY | PI_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.
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 herePP_* 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.
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.
| Key | Prefix |
|---|---|
PI_INBOUND_API_KEY | ppin_ |
PI_VLLM_API_KEY | ppvl_ |
PI_RAG_API_KEY | pprg_ |
PI_LLM_API_KEY | sk-pplm_ |
PI_ADMIN_SESSION_SECRET | ppss_ (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 platformPI_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.
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.
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.
| Order | What it does |
|---|---|
| 1 | Start 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) |
| 3 | docker 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
| Where | State | What 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 instance | Check 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 value | Check 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».
| Order | What it does |
|---|---|
| 1 | Place 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 |
| 2 | bin/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 |
| 3 | Clear 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.
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.
Related documents
- Environment Variable Configuration — Full list of configuration variables
- Service Operations — Start · stop · status check
- Database Management — Neo4j · PostgreSQL · Qdrant operations