fix(mcp): set the service account password on every run, not only at creation

Four roles created their MCP service account and never touched its password
again, while the same play authenticates as that account moments later. A
deployment whose declared password moved on therefore failed at the login,
not at the point the drift was introduced.

gitea now runs 'admin user change-password' before the token dance. The
--must-change-password=false flag is mandatory rather than cosmetic: at the
pinned v1.27.1 the flag defaults to true, so omitting it locks the account out
of the very API call that follows.

nextcloud resets through 'occ user:resetpassword --password-from-env' before
minting the app token that verifies the password.

snipe-it compared nothing at all - its docstring promised to pin the password
while the assignment sat inside the create branch. It now rehashes only when
Hash::check says the stored hash no longer matches, so the changed flag stays
honest.

home assistant swallowed a failed credential creation for accounts that already
existed and then logged in with them. It now calls admin_change_password and
falls back to creation only when the reply is credentials_not_found.

jellyfin and zammad already converged; wordpress, gitlab and moodle never
authenticate with this password and are left alone.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
kevinveenbirkenbachandClaude Opus 5 committed 2026-08-12 21:34:30 +02:00
1 parent 9a72d01eb6
commit e3caa074b2
4 files changed
+52 -2

No files matched your search

@@ -23,6 +23,19 @@
run_once: true
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
- name: "🔑 Set the account's password to the one this deployment declares"
ansible.builtin.shell: | # nocheck: container-exec-resolver runtime-resolved CID, delegated to the hosting node
container exec -i --user {{ GITEA_USER }} {{ GITEA_API_LOCAL_CID | quote }} \
/app/gitea/gitea admin user change-password \
--username {{ GITEA_MCP_USERNAME | quote }} \
--password {{ GITEA_MCP_USER_PASSWORD | quote }} \
--must-change-password=false \
-c {{ GITEA_CONFIG }}
changed_when: true
delegate_to: "{{ GITEA_API_HOST_NODE }}"
run_once: true
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
- name: "🗑 Revoke an access token left over under the same name"
ansible.builtin.shell: | # nocheck: container-exec-resolver runtime-resolved CID, delegated to the hosting node
container exec -i {{ GITEA_API_LOCAL_CID | quote }} \
@@ -159,6 +159,19 @@ async def ensure_service_account(admin_token):
raise SystemExit("MCP account refused: " + json.dumps(created))
user_id = created["result"]["user"]["id"]
if existing:
changed = await command(
{
"type": "config/auth_provider/homeassistant/admin_change_password",
"user_id": user_id,
"password": SERVICE_PASSWORD,
}
)
if changed.get("success"):
return False
if (changed.get("error") or {}).get("code") != "credentials_not_found":
raise SystemExit("MCP account password refused: " + json.dumps(changed))
credentials = await command(
{
"type": "config/auth_provider/homeassistant/create",
@@ -167,7 +180,7 @@ async def ensure_service_account(admin_token):
"password": SERVICE_PASSWORD,
}
)
if not credentials.get("success") and not existing:
if not credentials.get("success"):
raise SystemExit("MCP account credentials refused")
return not existing
@@ -38,6 +38,26 @@
delegate_to: "{{ NEXTCLOUD_HOST_NODE }}"
run_once: true
- name: "🔑 Set the account's password to the one this deployment declares"
ansible.builtin.command:
argv:
- container
- exec
- -u
- "{{ NEXTCLOUD_DOCKER_USER }}"
- -e
- "OC_PASS={{ NEXTCLOUD_MCP_USER_PASSWORD }}"
- "{{ NEXTCLOUD_LOCAL_CID }}" # nocheck: container-exec-resolver
- php
- "{{ NEXTCLOUD_INTERNAL_OCC_COMMAND }}"
- user:resetpassword
- --password-from-env
- "{{ NEXTCLOUD_MCP_USERNAME }}"
changed_when: true
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
delegate_to: "{{ NEXTCLOUD_HOST_NODE }}"
run_once: true
- name: "🔑 Mint an app password for the MCP clients"
ansible.builtin.command:
argv:
@@ -90,8 +90,12 @@ if (!$user) {
$user->first_name = "MCP";
$user->last_name = "Adapter";
$user->username = $username;
$user->password = bcrypt($password);
$user->activated = 1;
$user->password = bcrypt($password);
$user->save();
$changed = true;
} elseif (!\Illuminate\Support\Facades\Hash::check($password, (string) $user->password)) {
$user->password = bcrypt($password);
$user->save();
$changed = true;
}