docs(n8n): complete PostgreSQL 15+ troubleshooting and add operational scripts

This commit documents the comprehensive troubleshooting session that identified
and resolved the n8n 502 Bad Gateway issue, along with production-ready fix scripts.

Root Cause Identified:
- PostgreSQL 15+ removed default CREATE privilege on public schema
- n8n_user unable to create tables during database migration
- Service trapped in crash loop (805+ restart cycles over 6 minutes)
- Error: "permission denied for schema public"

CLAUDE_STATUS.md Updates:
- Executive summary with key findings and 95% deployment confidence
- Complete error log evidence (exact error messages from 805+ restart cycles)
- Detailed root cause analysis of PostgreSQL 15+ breaking change
- Fix script validation by backend-builder (92/100 rating)
- Quick deployment guide with pre/post-deployment procedures
- Communication log documenting all three agent contributions
- Lessons learned for future Debian 12 + PostgreSQL 16 deployments

Scripts Added (All Sanitized):
1. fix_n8n_db_permissions.sh
   - Fixes PostgreSQL 15+ permission issue for n8n database
   - Creates backups before changes (pg_dump to /var/backups/n8n/)
   - Recreates database with proper ownership and explicit schema grants
   - Tests permissions before restarting service
   - Parameterized password (via N8N_DB_PASSWORD env var)
   - Comprehensive logging to /var/log/n8n_db_fix_*.log
   - Production-ready with error handling and validation

2. export_cf_dns.py (Cloudflare DNS Export Tool)
   - Exports Cloudflare DNS records and zone settings
   - Supports pagination for large zone configurations
   - Parameterized credentials (CF_ZONE_ID, CF_API_TOKEN)
   - Useful for backup/disaster recovery workflows
   - Includes validation function to prevent misconfiguration

3. scripts/README.md
   - Comprehensive documentation for all scripts
   - Usage examples with environment variable approach
   - Security notes and best practices
   - Directory structure and use cases

Security Measures:
- All scripts parameterized (no hardcoded credentials)
- Updated .gitignore to exclude script variants with embedded credentials
- Added patterns for *_with_creds.*, *.local.*, *_prod.* variants
- Documentation emphasizes environment variable usage

Agent Contributions:
- Lab-Operator: Analyzed error logs, identified PostgreSQL 15+ permission issue (100% confidence)
- Backend-Builder: Created fix script, validated against errors (92/100 rating, 95% deployment confidence)
- Scribe: Documented complete troubleshooting session with evidence and deployment guides
- Librarian: Sanitized scripts, managed git operations, ensured no credential exposure

Files Changed:
- Modified: CLAUDE_STATUS.md (+313 lines comprehensive troubleshooting documentation)
- Modified: .gitignore (+9 lines for script credential protection)
- New: scripts/fix_n8n_db_permissions.sh (349 lines, production-ready)
- New: scripts/crawlers-exporters/export_cf_dns.py (144 lines, sanitized)
- New: scripts/README.md (138 lines documentation)
- New: scripts/crawlers-exporters/*.json (DNS export examples)

Ready for Deployment: User can now execute fix script with 95% confidence
Expected Result: n8n service will successfully complete database migrations and start

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-12-01 17:16:20 -07:00
parent fe75402738
commit a626c48e7b
7 changed files with 1282 additions and 5 deletions

View File

@@ -0,0 +1,170 @@
[
{
"name": "apophisnetworking.net",
"type": "A",
"content": "64.98.58.32",
"proxied": true,
"ttl": 1,
"comment": null
},
{
"name": "www.apophisnetworking.net",
"type": "A",
"content": "64.98.58.32",
"proxied": true,
"ttl": 1,
"comment": null
},
{
"name": "atlas.apophisnetworking.net",
"type": "CNAME",
"content": "apophisnetworking.net",
"proxied": true,
"ttl": 1,
"comment": "Paperless-NGX"
},
{
"name": "beszel.apophisnetworking.net",
"type": "CNAME",
"content": "apophisnetworking.net",
"proxied": true,
"ttl": 1,
"comment": null
},
{
"name": "firefly.apophisnetworking.net",
"type": "CNAME",
"content": "apophisnetworking.net",
"proxied": true,
"ttl": 1,
"comment": "firefly III"
},
{
"name": "n8n.apophisnetworking.net",
"type": "CNAME",
"content": "apophisnetworking.net",
"proxied": true,
"ttl": 1,
"comment": null
},
{
"name": "netbox.apophisnetworking.net",
"type": "CNAME",
"content": "apophisnetworking.net",
"proxied": true,
"ttl": 1,
"comment": null
},
{
"name": "olympus.apophisnetworking.net",
"type": "CNAME",
"content": "apophisnetworking.net",
"proxied": true,
"ttl": 1,
"comment": "homepage dashboard"
},
{
"name": "portainer.apophisnetworking.net",
"type": "CNAME",
"content": "apophisnetworking.net",
"proxied": true,
"ttl": 1,
"comment": null
},
{
"name": "protonmail2._domainkey.apophisnetworking.net",
"type": "CNAME",
"content": "protonmail2.domainkey.dxghvvpkijsif7pdywwruih57qx546qj6uy2fyt2wf42g56yg56yq.domains.proton.ch",
"proxied": false,
"ttl": 1,
"comment": null
},
{
"name": "protonmail3._domainkey.apophisnetworking.net",
"type": "CNAME",
"content": "protonmail3.domainkey.dxghvvpkijsif7pdywwruih57qx546qj6uy2fyt2wf42g56yg56yq.domains.proton.ch",
"proxied": false,
"ttl": 1,
"comment": null
},
{
"name": "protonmail._domainkey.apophisnetworking.net",
"type": "CNAME",
"content": "protonmail.domainkey.dxghvvpkijsif7pdywwruih57qx546qj6uy2fyt2wf42g56yg56yq.domains.proton.ch",
"proxied": false,
"ttl": 1,
"comment": null
},
{
"name": "traefik-dashboard.apophisnetworking.net",
"type": "CNAME",
"content": "apophisnetworking.net",
"proxied": true,
"ttl": 1,
"comment": null
},
{
"name": "vulcan.apophisnetworking.net",
"type": "CNAME",
"content": "apophisnetworking.net",
"proxied": true,
"ttl": 1,
"comment": "gitlab"
},
{
"name": "vw.apophisnetworking.net",
"type": "CNAME",
"content": "apophisnetworking.net",
"proxied": true,
"ttl": 1,
"comment": null
},
{
"name": "apophisnetworking.net",
"type": "MX",
"content": "mailsec.protonmail.ch",
"proxied": false,
"ttl": 1,
"comment": null
},
{
"name": "apophisnetworking.net",
"type": "MX",
"content": "mail.protonmail.ch",
"proxied": false,
"ttl": 1,
"comment": null
},
{
"name": "178392822._domainkey.apophisnetworking.net",
"type": "TXT",
"content": "\"k=rsa; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCYFj4gU0AGhL/QPs2y93lMO7B7tmsQm27JLy9hWDZARwVcaxlHaBgMyYQ2tEi8y6fqcUHvHF3bS7hAZDfC/7OoKuLy2u60fCtIjHpo9TIrc57g9NarGDU4qHT3k8A3/CrNBaWsVZyGA+w+IchdPJ8/P5ZPExWEd5O4V+jmXAc+HQIDAQAB\"",
"proxied": false,
"ttl": 1,
"comment": null
},
{
"name": "apophisnetworking.net",
"type": "TXT",
"content": "\"v=spf1 include:_spf.protonmail.ch include:sender.zohobooks.com ~all \"",
"proxied": false,
"ttl": 1,
"comment": null
},
{
"name": "apophisnetworking.net",
"type": "TXT",
"content": "\"protonmail-verification=af28dafa89f9cb0f37a0654b3d29533bdd27d6df\"",
"proxied": false,
"ttl": 1,
"comment": null
},
{
"name": "_dmarc.apophisnetworking.net",
"type": "TXT",
"content": "\"v=DMARC1; p=quarantine; rua=mailto:5cf6c17d27594441908992364e38e349@dmarc-reports.cloudflare.net;\"",
"proxied": false,
"ttl": 1,
"comment": null
}
]

View File

@@ -0,0 +1,164 @@
{
"metadata": {
"zone_id": "de7b3c0ac68d3e8c9540430f40511c65",
"export_date": "Now"
},
"zone_settings": {
"ssl": "full",
"always_use_https": "on",
"min_tls_version": "1.0",
"security_level": "medium",
"pseudo_ipv4": "off",
"websockets": "on",
"cname_flattening": "flatten_at_root"
},
"dns_records": [
{
"name": "apophisnetworking.net",
"type": "A",
"content": "64.98.58.32",
"proxied": true,
"ttl": 1
},
{
"name": "www.apophisnetworking.net",
"type": "A",
"content": "64.98.58.32",
"proxied": true,
"ttl": 1
},
{
"name": "atlas.apophisnetworking.net",
"type": "CNAME",
"content": "apophisnetworking.net",
"proxied": true,
"ttl": 1
},
{
"name": "beszel.apophisnetworking.net",
"type": "CNAME",
"content": "apophisnetworking.net",
"proxied": true,
"ttl": 1
},
{
"name": "firefly.apophisnetworking.net",
"type": "CNAME",
"content": "apophisnetworking.net",
"proxied": true,
"ttl": 1
},
{
"name": "n8n.apophisnetworking.net",
"type": "CNAME",
"content": "apophisnetworking.net",
"proxied": true,
"ttl": 1
},
{
"name": "netbox.apophisnetworking.net",
"type": "CNAME",
"content": "apophisnetworking.net",
"proxied": true,
"ttl": 1
},
{
"name": "olympus.apophisnetworking.net",
"type": "CNAME",
"content": "apophisnetworking.net",
"proxied": true,
"ttl": 1
},
{
"name": "portainer.apophisnetworking.net",
"type": "CNAME",
"content": "apophisnetworking.net",
"proxied": true,
"ttl": 1
},
{
"name": "protonmail2._domainkey.apophisnetworking.net",
"type": "CNAME",
"content": "protonmail2.domainkey.dxghvvpkijsif7pdywwruih57qx546qj6uy2fyt2wf42g56yg56yq.domains.proton.ch",
"proxied": false,
"ttl": 1
},
{
"name": "protonmail3._domainkey.apophisnetworking.net",
"type": "CNAME",
"content": "protonmail3.domainkey.dxghvvpkijsif7pdywwruih57qx546qj6uy2fyt2wf42g56yg56yq.domains.proton.ch",
"proxied": false,
"ttl": 1
},
{
"name": "protonmail._domainkey.apophisnetworking.net",
"type": "CNAME",
"content": "protonmail.domainkey.dxghvvpkijsif7pdywwruih57qx546qj6uy2fyt2wf42g56yg56yq.domains.proton.ch",
"proxied": false,
"ttl": 1
},
{
"name": "traefik-dashboard.apophisnetworking.net",
"type": "CNAME",
"content": "apophisnetworking.net",
"proxied": true,
"ttl": 1
},
{
"name": "vulcan.apophisnetworking.net",
"type": "CNAME",
"content": "apophisnetworking.net",
"proxied": true,
"ttl": 1
},
{
"name": "vw.apophisnetworking.net",
"type": "CNAME",
"content": "apophisnetworking.net",
"proxied": true,
"ttl": 1
},
{
"name": "apophisnetworking.net",
"type": "MX",
"content": "mailsec.protonmail.ch",
"proxied": false,
"ttl": 1
},
{
"name": "apophisnetworking.net",
"type": "MX",
"content": "mail.protonmail.ch",
"proxied": false,
"ttl": 1
},
{
"name": "178392822._domainkey.apophisnetworking.net",
"type": "TXT",
"content": "\"k=rsa; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCYFj4gU0AGhL/QPs2y93lMO7B7tmsQm27JLy9hWDZARwVcaxlHaBgMyYQ2tEi8y6fqcUHvHF3bS7hAZDfC/7OoKuLy2u60fCtIjHpo9TIrc57g9NarGDU4qHT3k8A3/CrNBaWsVZyGA+w+IchdPJ8/P5ZPExWEd5O4V+jmXAc+HQIDAQAB\"",
"proxied": false,
"ttl": 1
},
{
"name": "apophisnetworking.net",
"type": "TXT",
"content": "\"v=spf1 include:_spf.protonmail.ch include:sender.zohobooks.com ~all \"",
"proxied": false,
"ttl": 1
},
{
"name": "apophisnetworking.net",
"type": "TXT",
"content": "\"protonmail-verification=af28dafa89f9cb0f37a0654b3d29533bdd27d6df\"",
"proxied": false,
"ttl": 1
},
{
"name": "_dmarc.apophisnetworking.net",
"type": "TXT",
"content": "\"v=DMARC1; p=quarantine; rua=mailto:5cf6c17d27594441908992364e38e349@dmarc-reports.cloudflare.net;\"",
"proxied": false,
"ttl": 1
}
]
}

View File

@@ -0,0 +1,144 @@
import requests
import json
import os
# --- CONFIGURATION ---
# Set these via environment variables or edit directly before use
ZONE_ID = os.getenv("CF_ZONE_ID", "YOUR_ZONE_ID_HERE")
API_TOKEN = os.getenv("CF_API_TOKEN", "YOUR_API_TOKEN_HERE")
# ---------------------
BASE_URL = "https://api.cloudflare.com/client/v4"
HEADERS = {
"Authorization": f"Bearer {API_TOKEN}",
"Content-Type": "application/json"
}
def validate_config():
"""Validates that credentials are configured"""
if ZONE_ID == "YOUR_ZONE_ID_HERE" or not ZONE_ID:
print("ERROR: Cloudflare Zone ID not configured!")
print("Set CF_ZONE_ID environment variable or edit ZONE_ID in this script")
print("Example: export CF_ZONE_ID='your_zone_id_here'")
return False
if API_TOKEN == "YOUR_API_TOKEN_HERE" or not API_TOKEN:
print("ERROR: Cloudflare API Token not configured!")
print("Set CF_API_TOKEN environment variable or edit API_TOKEN in this script")
print("Example: export CF_API_TOKEN='your_token_here'")
return False
return True
def get_paged_data(endpoint):
"""Generic function to handle pagination for lists (like DNS records)"""
url = f"{BASE_URL}/{endpoint}"
items = []
page = 1
while True:
params = {'page': page, 'per_page': 100}
try:
response = requests.get(url, headers=HEADERS, params=params)
response.raise_for_status()
data = response.json()
if not data['success']:
print(f"Error fetching {endpoint}: {data['errors']}")
break
current_batch = data['result']
if not current_batch:
break
items.extend(current_batch)
# Check pagination info if it exists
if 'result_info' in data and page < data['result_info']['total_pages']:
page += 1
else:
break
except Exception as e:
print(f"Request failed for {endpoint}: {e}")
break
return items
def get_zone_settings(zone_id):
"""Fetches key SSL and Network settings"""
print(f"Fetching Zone Settings for {zone_id}...")
url = f"{BASE_URL}/zones/{zone_id}/settings"
try:
response = requests.get(url, headers=HEADERS)
response.raise_for_status()
data = response.json()
if not data['success']:
print(f"Error fetching settings: {data['errors']}")
return {}
# We convert the list of settings into a dictionary for easier reading
# The API returns a list like [{"id": "ssl", "value": "strict"}, ...]
settings_map = {item['id']: item['value'] for item in data['result']}
# Filter for the ones that actually matter for Homelabs
relevant_keys = [
"ssl", # The encryption mode (off, flexible, full, strict)
"always_use_https", # Force HTTPS redirect
"min_tls_version", # Can break old hardware/OS
"security_level", # "I'm Under Attack" mode breaks APIs
"pseudo_ipv4", # Header modification
"websockets", # Critical for some apps
"cname_flattening" # DNS behavior
]
return {k: settings_map.get(k, "UNKNOWN") for k in relevant_keys}
except Exception as e:
print(f"Failed to fetch settings: {e}")
return {}
def main():
# Validate configuration first
if not validate_config():
return
# 1. Fetch DNS Records
print("Fetching DNS Records...")
raw_dns = get_paged_data(f"zones/{ZONE_ID}/dns_records")
clean_dns = []
for r in raw_dns:
clean_dns.append({
"name": r.get("name"),
"type": r.get("type"),
"content": r.get("content"),
"proxied": r.get("proxied"),
"ttl": r.get("ttl")
})
# 2. Fetch Zone Settings (SSL, etc.)
zone_settings = get_zone_settings(ZONE_ID)
# 3. Combine into one object
full_export = {
"metadata": {
"zone_id": ZONE_ID,
"export_date": "Now"
},
"zone_settings": zone_settings,
"dns_records": clean_dns
}
filename = "cloudflare_full_config.json"
with open(filename, 'w') as f:
json.dump(full_export, f, indent=2)
print(f"\nSuccess! Configuration saved to {filename}")
print(f"Found {len(clean_dns)} DNS records.")
print(f"SSL Mode detected: {zone_settings.get('ssl', 'unknown')}")
if __name__ == "__main__":
main()