mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-06 02:54:26 +00:00
fix(n8n): carry a rotated owner password into the account n8n already has
n8n accepts the owner password only at /rest/owner/setup, which answers 400 "Instance owner already setup" once an owner exists. After the CI rotated owner_password between the passes, the probe got 401, setup got 400, the assert accepted "already setup", and the owner kept the old password: the Playwright owner login failed in n8n#1 and the AI gateway login in n8n#0 got 401 and 429 (run 34615516009, jobs 103327034581 and 103327034619). Only in exactly that state, probe 401/403 plus "already setup", a node script now runs inside the n8n container with n8n's own modules and DB_POSTGRESDB_* settings: it compares the stored bcrypt hash with the deployed password, writes a fresh bcryptjs hash when they differ, and prints CHANGED or OK. A single confirming login then requires 200, so a failed write stops the deploy with a clear status. Fresh hosts and deploys whose password already matches never run the script. The pattern follows web-app-flowise's identity_password.js. The module path was measured in n8nio/n8n:1.100.1: bcryptjs and pg resolve from /usr/local/lib/node_modules/n8n/node_modules, pg's transitive modules load, and bcryptjs hashes as $2a$ like n8n. no_log moves from the bootstrap include's apply to the four tasks that carry the password, so the setup assert and the host and container resolution stay readable when they fail. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
a20883600b
commit
43e09ebf0d
4 files changed
+114
-1
No files matched your search
@@ -0,0 +1,62 @@
|
||||
/**
|
||||
* Prints CHANGED when it rewrote the owner's hash, OK when it already matched or
|
||||
* the owner does not exist yet.
|
||||
*
|
||||
* Environment:
|
||||
* N8N_OWNER_EMAIL: the owner account's email.
|
||||
* N8N_OWNER_PASSWORD: the password the deployment wants it to have.
|
||||
* DB_TYPE, DB_POSTGRESDB_HOST, DB_POSTGRESDB_PORT, DB_POSTGRESDB_DATABASE,
|
||||
* DB_POSTGRESDB_USER, DB_POSTGRESDB_PASSWORD: n8n's own connection settings.
|
||||
*/
|
||||
|
||||
// nocheck: mirrored-unit-test - resolves bcryptjs and pg from the n8n container's
|
||||
// own node_modules and writes the hash straight into the live user row; outside that
|
||||
// container the requires do not resolve
|
||||
|
||||
const bcrypt = require('bcryptjs')
|
||||
const { Client } = require('pg')
|
||||
|
||||
const email = process.env.N8N_OWNER_EMAIL
|
||||
const password = process.env.N8N_OWNER_PASSWORD
|
||||
|
||||
function fail(message) {
|
||||
process.stderr.write(`FAILED ${message}\n`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
async function main() {
|
||||
if (!email || !password) {
|
||||
fail('N8N_OWNER_EMAIL and N8N_OWNER_PASSWORD are required')
|
||||
}
|
||||
if (process.env.DB_TYPE !== 'postgresdb') {
|
||||
fail(`unsupported DB_TYPE ${process.env.DB_TYPE}`)
|
||||
}
|
||||
|
||||
const client = new Client({
|
||||
host: process.env.DB_POSTGRESDB_HOST,
|
||||
port: parseInt(process.env.DB_POSTGRESDB_PORT || '5432', 10),
|
||||
database: process.env.DB_POSTGRESDB_DATABASE,
|
||||
user: process.env.DB_POSTGRESDB_USER,
|
||||
password: process.env.DB_POSTGRESDB_PASSWORD
|
||||
})
|
||||
await client.connect()
|
||||
try {
|
||||
const found = await client.query('SELECT password FROM "user" WHERE email = lower($1)', [email])
|
||||
if (found.rowCount === 0) {
|
||||
console.log('OK')
|
||||
return
|
||||
}
|
||||
const stored = found.rows[0].password
|
||||
if (stored && bcrypt.compareSync(password, stored)) {
|
||||
console.log('OK')
|
||||
return
|
||||
}
|
||||
const hash = bcrypt.hashSync(password, 10)
|
||||
await client.query('UPDATE "user" SET password = $1 WHERE email = lower($2)', [hash, email])
|
||||
console.log('CHANGED')
|
||||
} finally {
|
||||
await client.end()
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((error) => fail(error.message))
|
||||
@@ -43,7 +43,6 @@
|
||||
include_tasks:
|
||||
file: 01_bootstrap.yml
|
||||
apply:
|
||||
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
|
||||
run_once: true
|
||||
|
||||
- name: "🤖 Bind the native AI surface to the platform gateway"
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
until: "'json' in (n8n_owner_probe.content_type | default(''))"
|
||||
retries: 4
|
||||
delay: 30
|
||||
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
|
||||
|
||||
- name: "🔧 Initialise n8n owner account (first-time only)"
|
||||
uri_retry:
|
||||
@@ -44,6 +45,7 @@
|
||||
return_content: true
|
||||
register: n8n_owner_setup
|
||||
when: n8n_owner_probe.status != 200
|
||||
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
|
||||
|
||||
- name: "✅ Assert n8n owner setup succeeded or was already done"
|
||||
ansible.builtin.assert:
|
||||
@@ -53,3 +55,52 @@
|
||||
or (n8n_owner_setup.status in [200, 201])
|
||||
or ('already setup' in (n8n_owner_setup.content | default('')))
|
||||
fail_msg: "n8n owner setup returned {{ n8n_owner_setup.status | default('n/a') }}: {{ n8n_owner_setup.content | default('') }}"
|
||||
|
||||
- name: "🔐 Converge a rotated owner password n8n still rejects"
|
||||
when:
|
||||
- n8n_owner_probe.status in [401, 403]
|
||||
- "'already setup' in (n8n_owner_setup.content | default(''))"
|
||||
block:
|
||||
- name: "🔍 Resolve {{ application_id }} hosting node + container id"
|
||||
ansible.builtin.include_tasks: "{{ lookup('path_absolute', 'roles/sys-svc-compose/tasks/utils/swarm/resolve_host_cid.yml') }}"
|
||||
vars:
|
||||
resolve_service_key: "{{ entity_name }}"
|
||||
resolve_output_prefix: N8N
|
||||
resolve_compose_cid: "{{ N8N_CONTAINER }}"
|
||||
|
||||
- name: "🔐 Write the deployed owner password into n8n's user row"
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- container
|
||||
- exec
|
||||
- -i
|
||||
- -e
|
||||
- "NODE_PATH={{ N8N_NODE_MODULES }}"
|
||||
- -e
|
||||
- "N8N_OWNER_EMAIL={{ N8N_OWNER_EMAIL }}"
|
||||
- -e
|
||||
- "N8N_OWNER_PASSWORD={{ N8N_OWNER_PASSWORD }}"
|
||||
- "{{ N8N_LOCAL_CID }}" # nocheck: container-exec-resolver
|
||||
- node
|
||||
- "-"
|
||||
stdin: "{{ lookup('file', 'javascript/reconcile/owner_password.js') }}"
|
||||
register: n8n_owner_password
|
||||
retries: 30
|
||||
delay: 10
|
||||
until: n8n_owner_password.rc == 0
|
||||
changed_when: n8n_owner_password.stdout.startswith('CHANGED')
|
||||
delegate_to: "{{ N8N_HOST_NODE }}"
|
||||
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
|
||||
|
||||
- name: "🔎 Confirm the owner signs in with the deployed password"
|
||||
uri_retry:
|
||||
url: "{{ N8N_INTERNAL_API_URL }}/rest/login"
|
||||
method: POST
|
||||
body_format: json
|
||||
body:
|
||||
emailOrLdapLoginId: "{{ lookup('users', 'administrator').email }}"
|
||||
password: "{{ N8N_OWNER_PASSWORD }}"
|
||||
status_code: [200]
|
||||
return_content: false
|
||||
retries: 0
|
||||
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
|
||||
@@ -10,6 +10,7 @@ N8N_HOSTNAME: "{{ lookup('domain', application_id) }}"
|
||||
N8N_WEB_PROTOCOL: "{{ lookup('tls', application_id, 'protocols.web') }}"
|
||||
N8N_ENCRYPTION_KEY: "{{ lookup('config', application_id, 'secrets.credentials.encryption_key') }}"
|
||||
N8N_OWNER_PASSWORD: "{{ lookup('config', application_id, 'secrets.credentials.owner_password') }}"
|
||||
N8N_NODE_MODULES: /usr/local/lib/node_modules/n8n/node_modules
|
||||
|
||||
N8N_INTERNAL_API_URL: "http://{{ DOCKER_REACH_HOST }}:{{ N8N_HTTP_PORT }}"
|
||||
|
||||
|
||||
Reference in new issue
Block a user