Files
computer-playbook/roles/web-app-gitea/tasks/utils/mcp/provision.yml
kevinveenbirkenbachandClaude Opus 5 e3caa074b2 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>
2026-08-12 21:34:30 +02:00

91 lines
4.1 KiB
YAML

---
- name: "🔍 List the accounts this instance knows"
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 list -c {{ GITEA_CONFIG }}
register: gitea_mcp_accounts
changed_when: false
delegate_to: "{{ GITEA_API_HOST_NODE }}"
run_once: true
- name: "👤 Create the account the MCP token belongs to"
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 create \
--username {{ GITEA_MCP_USERNAME | quote }} \
--password {{ GITEA_MCP_USER_PASSWORD | quote }} \
--email {{ GITEA_MCP_USER_EMAIL | quote }} \
--must-change-password=false \
-c {{ GITEA_CONFIG }}
when: GITEA_MCP_USERNAME not in gitea_mcp_accounts.stdout.split()
changed_when: true
delegate_to: "{{ GITEA_API_HOST_NODE }}"
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 }} \
{{ lookup('timeout', 30) | curl(5, 0) }} -o /dev/null -w '%{http_code}' \
-X DELETE \
-u {{ (GITEA_MCP_USERNAME ~ ':' ~ GITEA_MCP_USER_PASSWORD) | quote }} \
{{ (GITEA_INTERNAL_API_URL ~ '/users/' ~ GITEA_MCP_USERNAME ~ '/tokens/' ~ GITEA_MCP_TOKEN_NAME) | quote }}
register: gitea_mcp_token_revoke
changed_when: gitea_mcp_token_revoke.stdout | trim == '204'
failed_when: gitea_mcp_token_revoke.stdout | trim not in ['204', '404']
delegate_to: "{{ GITEA_API_HOST_NODE }}"
run_once: true
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
- name: "🔑 Mint the personal access token MCP clients present"
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 generate-access-token \
--username {{ GITEA_MCP_USERNAME | quote }} \
--token-name {{ GITEA_MCP_TOKEN_NAME | quote }} \
--scopes {{ GITEA_MCP_TOKEN_SCOPES | quote }} \
--raw \
-c {{ GITEA_CONFIG }}
register: gitea_mcp_mint
changed_when: true
delegate_to: "{{ GITEA_API_HOST_NODE }}"
run_once: true
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
- name: "🔧 Take the minted token from the mint output"
ansible.builtin.set_fact:
gitea_mcp_token: "{{ gitea_mcp_mint.stdout_lines | last | trim }}"
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
run_once: true
- name: "🛑 Refuse an instance that issued no usable token"
ansible.builtin.assert:
that:
- gitea_mcp_token | length >= 40
fail_msg: >-
Gitea returned no access token, so every MCP client would be configured
with an empty bearer and fail at first use.
- name: "💾 Persist the token so MCP clients can read it"
ansible.builtin.include_role:
name: sys-token-store
tasks_from: write.yml
vars:
sys_token_store_user_key: "{{ lookup('config', application_id, 'mcp.credential.owner') }}"
sys_token_store_app: "{{ application_id }}"
sys_token_store_token: "{{ gitea_mcp_token }}"
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"