mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-06 10:59:59 +00:00
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>
91 lines
4.1 KiB
YAML
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 }}"
|