./discourse-setup plaatst SMTP-gebruikersnaam aan het begin van SMTP-wachtwoord

Environment

  • Discourse: latest tests-passed
  • Host OS: [Ubuntu 24.04 LTS / 24.04.3 LTS]
  • Install method: official Docker install, running ./discourse-setup
  • Platform: VPS (Digital Ocean)

Steps to reproduce

  1. Run ./discourse-setup from /var/discourse
  2. Enter SMTP details when prompted:
  1. Finish setup and inspect containers/app.yml

Expected result

app.yml should contain two distinct fields, for example:

DISCOURSE_SMTP_USER_NAME: "user@example.com"
DISCOURSE_SMTP_PASSWORD: "p@ssw0rd!"

Actual result

The password field is written with the username prepended:

DISCOURSE_SMTP_USER_NAME: "user@example.com"
DISCOURSE_SMTP_PASSWORD: "user@brevo.comp@ssw0rd!"

This causes mail delivery to fail, and discourse-doctor confirms the mangled password.

Notes
• Quoting the password explicitly at the prompt does not change the outcome.
• Password contains special characters (@, !), but the problem is not just YAML quoting: the username string is literally concatenated at the start of the password.
• Reproducible across multiple runs of ./discourse-setup.

1 like

kun je testen of dit verband houdt met bepaalde tekens in het wachtwoord, zoals @ / ! en of dit de bug aan het licht brengt?

Alle sed/awk en fancy bash scripting in discourse-setup maken het nogal lastig te onderhouden. Misschien heeft @pfaffman hier ideeën over?

1 like

Oké. Dat is een goede gok.

@Ethsim2 als je het wachtwoord verandert in iets met alleen cijfers en letters en niet hetzelfde als een deel van de gebruikersnaam, heb je dan dit probleem?

Kun je de waarden delen die je gebruikt?

Ik zal er later vandaag naar kijken.

Als je het 83f…com-gedeelte van het wachtwoord zou verwijderen en alleen het wachtwoord (5AH…) zou laten staan, werkt het dan?

2 likes

ja, dan voer ik .\launcher rebuild app uit, aangezien .\discourse-setup SWAP al had geconfigureerd

Yeah. That’s the issue. There are multiple levels of escaping that need to happen, like when bash reads the value, when bash hands the value to sed, when sed replaces it, and then, maybe, when the yml file gets it. It’s a known issue:

I recategorized this as Support.

SMTP usernames always contain @ though, don’t they?

Ja. Ik denk niet dat @ een probleem zou moeten veroorzaken.

1 like

er staat geen ! in mijn screenshot waar de concatenatie plaatsvindt

Wat is het wachtwoord dat u probeert in te voeren?

the password on the screenshot is 5AHQXrf4LDUmRB1J :slightly_smiling_face:

Edit: this password is a default Brevo password created on an account that was since deleted

Ik kan het niet reproduceren. Welk besturingssysteem gebruik je?

root@bro:/var/discourse# ./discourse-setup --skip-connection-test --skip-rebuild
skipping connection test
'samples/standalone.yml' -> 'containers/app.yml'
Found 29GB of memory and 32 physical CPU cores
setting db_shared_buffers = 4096MB
setting UNICORN_WORKERS = 8
containers/app.yml memory parameters updated.

Hostname for your Discourse? [discourse.example.com]: forum.phsics.site

Setting EC to 2
Skipping port check.
Email address for admin account(s)? [me@example.com,you@example.com]: jay@literatecomputing.com
SMTP server address? [smtp.example.com]: smtp-relay.brevo.com
SMTP port? [587]: 2525
SMTP user name? [user@example.com]: 83fca0012@smtp-brevo.com
SMTP password? []: 5AHQXrf4LDUmRB1J
notification email address? [noreply@forum.phsics.site]:
Optional email address for Let's Encrypt warnings? (ENTER to skip) [me@example.com]:
Optional MaxMind Account ID (ENTER to continue without MAXMIND GeoLite2 geolocation database) [123456]:

Does this look right?

Hostname          : forum.phsics.site
Email             : jay@literatecomputing.com
SMTP address      : smtp-relay.brevo.com
SMTP port         : 2525
SMTP username     : 83fca0012@smtp-brevo.com
SMTP password     : 5AHQXrf4LDUmRB1J
Notification email: noreply@forum.phsics.site
MaxMind account ID: (unset)
MaxMind license key: (unset)

ENTER to continue, 'n' to try again, Ctrl+C to exit:
letsencrypt.ssl.template.yml enabled

Configuration file at containers/app.yml updated successfully!

Updates successful. --skip-rebuild requested. Exiting.
root@bro:/var/discourse# grep SMTP containers/app.yml
  ## TODO: The SMTP mail server used to validate new accounts and send notifications
  # SMTP ADDRESS is required
  # WARNING: SMTP password should be wrapped in quotes to avoid problems
  DISCOURSE_SMTP_ADDRESS: smtp-relay.brevo.com
  DISCOURSE_SMTP_PORT: 2525
  DISCOURSE_SMTP_USER_NAME: 83fca0012@smtp-brevo.com
  DISCOURSE_SMTP_PASSWORD: "5AHQXrf4LDUmRB1J"
  #DISCOURSE_SMTP_ENABLE_START_TLS: true           # (optional, default: true)
  DISCOURSE_SMTP_DOMAIN: discourse.example.com # (required by some providers)
  #DISCOURSE_SMTP_OPENSSL_VERIFY_MODE: peer        # (optional, default: peer, valid values: none, peer, client_once, fail_if_no_peer_cert)
  #DISCOURSE_SMTP_AUTHENTICATION: plain            # (default: plain, valid values: plain, login, cram_md5)
1 like

Digital Ocean Ubuntu 24.04

LLM/AI -gegenereerde aanbevelingen

\nhieronder staat een strakke patch die:\n\n- stopt met het gebruik van sed\n- construeert een percent-gecodeerde SMTP_URL\n- bewerkt containers/app.yml via Ruby’s YAML (Psych), zodat YAML-quoting/escaping wordt afgehandeld door een echte parser\n- verwijdert de per-sleutel SMTP-variabelen om tegenstrijdigheden te voorkomen\n\n toepassen met git apply -p0 in de discourse_docker repo.\n\n- - -\n\nPatch 1 -\n\ndiscourse-setup\n\n(schrijf SMTP met Ruby YAML, niet met sed)\n\ndiff\n--- a/discourse-setup\n+++ b/discourse-setup\n@@ -867,6 +867,77 @@ write_smtp_settings() {\n local app_yml=\"containers/app.yml\"\n [[ -f \"$app_yml\" ]] || die \"Cannot find $app_yml. Did you run bootstrap?\"\n\n+ # Bouw een URL-gecodeerde SMTP_URL met Python stdlib (geen shell escaping games)\n+ urlencode() {\n+ python3 - \u003c\u003c'PY'\n+import sys, urllib.parse\n+print(urllib.parse.quote(sys.stdin.read().strip(), safe='._~-'))\n+PY\n+ }\n+\n+ # BELANGRIJK: lees variabelen zonder backslash-vervorming\n+ # (deze komen van eerdere prompts; zorg ervoor dat -r wordt gebruikt bij het prompten)\n+ local addr=\"$smtp_address\"\n+ local port=\"$smtp_port\"\n+ local user_enc pass_enc\n+ user_enc=\"$(printf '%s' \"$smtp_user\" | urlencode)\"\n+ pass_enc=\"$(printf '%s' \"$smtp_password\" | urlencode)\"\n+ local smtp_url=\"smtp://${user_enc}:${pass_enc}@${addr}:${port}\"\n+\n+ # Gebruik Ruby om veilig YAML te laden/bewerken/dumpen (doodt 3 lagen van escaping)\n+ ruby - \u003c\u003c'RUBY' \"$app_yml\" \"$smtp_url\"\n+require \"yaml\"\n+require \"psych\"\n+path, url = ARGV\n+doc = YAML.safe_load(File.read(path), permitted_classes: [], aliases: true) || {}\n+\n+# Zorg ervoor dat de top-level structuur een Hash is en env heeft\n+unless doc.is_a?(Hash)\n+ abort \"containers/app.yml parseert niet naar een Hash\"\n+end\n+doc[\"env\"] ||= {}\n+env = doc[\"env\"]\n+\n+# Schrijf enkele regel SMTP_URL; verwijder per-sleutel variabelen om conflicten te voorkomen\n+env[\"SMTP_URL\"] = url\n+%w[DISCOURSE_SMTP_ADDRESS DISCOURSE_SMTP_PORT DISCOURSE_SMTP_USER_NAME DISCOURSE_SMTP_PASSWORD].each { |k| env.delete(k) }\n+\n+# Dump terug. (Psych behoudt strings veilig geciteerd zoals nodig.)\n+File.write(path, Psych.dump(doc))\n+RUBY\n+\n+ # snelle sanity check voor de klassieke "wachtwoord voorafgegaan door gebruikersnaam" mislukking\n+ python3 - \u003c\u003c'PY'\n+import re, sys\n+y = open(\"containers/app.yml\",\"r\",encoding=\"utf-8\").read()\n+m = re.search(r'^\\s*SMTP_URL:\\s*(?:\"|\\')?([^\\r\\n\"\\']+)', y, re.M)\n+assert m, \"SMTP_URL ontbreekt na schrijven\"\n+creds = m.group(1).split('@',1)[0].split('://',1)[-1]\n+assert \":\" in creds, \"SMTP_URL creds missen ':'\"\n+u, p = creds.split(':',1)\n+assert not p.startswith(u), \"Wachtwoord lijkt voorafgegaan door gebruikersnaam\"\n+print(\"SMTP_URL ziet er gezond uit.\")\n+PY\n+}\n+\n- # Schrijf per-sleutel SMTP-invoer (adres/poort/gebruikersnaam/wachtwoord)\n- # (legacy: uitgevoerd via sed-substituties)\n- # OPMERKING: historisch fragiel met speciale tekens\n- update_setting_yaml \"DISCOURSE_SMTP_ADDRESS\" \"$smtp_address\"\n- update_setting_yaml \"DISCOURSE_SMTP_PORT\" \"$smtp_port\"\n- update_setting_yaml \"DISCOURSE_SMTP_USER_NAME\" \"$smtp_user\"\n- update_setting_yaml \"DISCOURSE_SMTP_PASSWORD\" \"$smtp_password\"\n-}\n+ # (legacy per-sleutel schrijfacties verwijderd ten gunste van SMTP_URL via YAML)\n+}\n\n\ndan Patch 2 -\n\ntemplates/web.template.yml\n\n(om het veiligere pad te documenteren)\n\ndiff\n--- a/templates/web.template.yml\n+++ b/templates/web.template.yml\n@@ -68,6 +68,14 @@ params:\n DISCOURSE_SMTP_ENABLE_START_TLS: true\n #DISCOURSE_NOTIFICATION_EMAIL: noreply@example.com\n\n+ ## Voorkeurs SMTP-configuratie in één regel (ingesteld door discourse-setup):\n+ ## URL-codeer gebruikersnaam en wachtwoord; voorbeeld:\n+ ## SMTP_URL: \"smtp://user%40example.com:p%40ss%3Aword@smtp.example.com:587\"\n+ ##\n+ #SMTP_URL:\n+\n ## Als u SMTP_URL niet kunt gebruiken, kunt u in plaats daarvan per-sleutel variabelen instellen.\n ## Pas op dat het bewerken van die regels met shell-tools fragiel kan zijn als waarden\n ## tekens bevatten zoals @, :, /, \", \\, of nieuwe regels.\n\n\nwaarom dit werkt (en wat het vermijdt)\n\t•\tbash-laag: we interpoleren alleen eenvoudige variabelen; geheimen worden via stdin/argv aan Python/Ruby doorgegeven, niet via sed-regexes of shell-evaluaties.\n\t•\tsed-laag: volledig verwijderd.\n\t•\tYAML-laag: Ruby/Psych behandelt quoting en escaping correct; geen handmatig gemaakte quoting.\n\t•\tSMTP-credentials: %-encoding in SMTP_URL is de juiste plaats om speciale tekens voor authenticatie te coderen.\n\nals u de voorkeur geeft aan het behouden van per-sleutel variabelen, kan ik u een zusterpatch geven die dezelfde Ruby-YAML-aanpak gebruikt om DISCOURSE_SMTP_* direct in te stellen (nog steeds geen sed), maar de SMTP_URL-route is de schoonste omdat het één sleutel, één schrijfactie, één coderingstap is.\n

We kunnen er helaas niet op vertrouwen dat Ruby is geïnstalleerd op het hostsysteem in discourse-installaties - het zou relatief eenvoudig zijn om het hele script een ruby-script te laten zijn als we dat konden, maar er is geen garantie.

2 likes
also LLM/AI generated

Got it — no Ruby/Python on the host. Here’s a pure Bash + awk patch that:
• builds a percent-encoded SMTP_URL using only Bash (byte-wise loop; no Python),
• inserts or replaces SMTP_URL under the env: block using awk (no sed),
• removes the per-key DISCOURSE_SMTP_* lines (safe anchored deletes),
• adds a tiny sanity check using grep/awk only.

Apply with git apply -p0 in the discourse_docker repo.

--- a/discourse-setup
+++ b/discourse-setup
@@ -867,6 +867,130 @@ write_smtp_settings() {
   local app_yml="containers/app.yml"
   [[ -f "$app_yml" ]] || die "Cannot find $app_yml. Did you run bootstrap?"

+  ##############################################
+  # Pure-Bash URL encoder for SMTP credentials #
+  ##############################################
+  # Encodes everything except A-Z a-z 0-9 . _ ~ -
+  # Works byte-by-byte; requires bash and printf.
+  urlencode_cred() {
+    local s="$1" out= i ch o
+    # set C locale to get byte semantics
+    LC_ALL=C
+    for ((i=0; i<${#s}; i++)); do
+      ch="${s:i:1}"
+      case "$ch" in
+        [A-Za-z0-9._~-])
+          out+="$ch"
+          ;;
+        *)
+          # Get byte value: print char, read with od, then format %HH
+          # Avoid external heavy deps; od is in coreutils / busybox.
+          o=$(printf '%s' "$ch" | od -An -tu1 | awk '{$1=$1;print $1}')
+          # If od somehow failed (empty), fall back to hex via printf %02X of first byte
+          if [ -z "$o" ]; then
+            o=$(printf '%s' "$ch" | head -c1 | od -An -tu1 | awk '{$1=$1;print $1}')
+          fi
+          printf -v o '%%%02X' "$o"
+          out+="$o"
+          ;;
+      esac
+    done
+    printf '%s' "$out"
+  }
+
+  # Build SMTP_URL (single line) from collected answers
+  # These vars are gathered earlier in discourse-setup:
+  #   $smtp_address  $smtp_port  $smtp_user  $smtp_password
+  local addr="$smtp_address"
+  local port="$smtp_port"
+  local user_enc pass_enc
+  user_enc="$(urlencode_cred "$smtp_user")"
+  pass_enc="$(urlencode_cred "$smtp_password")"
+  local smtp_url="smtp://${user_enc}:${pass_enc}@${addr}:${port}"
+
+  ########################################################
+  # YAML-safe edit via awk (no sed / no external runtimes)
+  # - ensure env: exists
+  # - insert or replace SMTP_URL under env:
+  # - remove DISCOURSE_SMTP_* keys
+  ########################################################
+  awk -v NEWVAL="$smtp_url" '
+    BEGIN{
+      have_env=0; in_env=0; inserted=0
+    }
+    # detect env: line at top level (start of line, possibly indented 0..)
+    # we’ll consider two-space indentation for children.
+    /^[[:space:]]*env:[[:space:]]*$/ {
+      print; have_env=1; in_env=1; next
+    }
+    # leaving env: block when indentation returns to 0 or next top-level key
+    in_env && /^[^[:space:]]/ {
+      if (!inserted) {
+        print "  SMTP_URL: \"" NEWVAL "\""
+        inserted=1
+      }
+      in_env=0
+    }
+    # while in env:, handle replacements and deletions
+    in_env {
+      # drop per-key DISCOURSE_SMTP_* lines entirely
+      if ($0 ~ /^[[:space:]]*DISCOURSE_SMTP_(ADDRESS|PORT|USER_NAME|PASSWORD):/) next
+      # replace existing SMTP_URL line
+      if ($0 ~ /^[[:space:]]*SMTP_URL:[[:space:]]*/) {
+        print "  SMTP_URL: \"" NEWVAL "\""
+        inserted=1
+        next
+      }
+      print
+      next
+    }
+    { print }
+    END{
+      # If env: never existed, append it with the key
+      if (!have_env) {
+        print ""
+        print "env:"
+        print "  SMTP_URL: \"" NEWVAL "\""
+      } else if (in_env && !inserted) {
+        # env: existed and we were still in it at EOF
+        print "  SMTP_URL: \"" NEWVAL "\""
+      }
+    }
+  ' "$app_yml" > "$app_yml.tmp.$$" && mv "$app_yml.tmp.$$" "$app_yml"
+
+  ##############################################
+  # Sanity check: basic guard against mangling #
+  ##############################################
+  # 1) SMTP_URL present
+  grep -q '^[[:space:]]*SMTP_URL:' "$app_yml" || die "SMTP_URL not written to $app_yml"
+  # 2) password not prefixed by username (classic failure signature)
+  awk '
+    BEGIN{ok=1}
+    /^[[:space:]]*SMTP_URL:[[:space:]]*"/{
+      line=$0
+      gsub(/^[[:space:]]*SMTP_URL:[[:space:]]*"/,"",line)
+      gsub(/".*$/,"",line)
+      # Extract creds before @ and after scheme
+      # e.g. smtp://user:pass@host:port
+      sub(/^[a-z]+:\/\//,"",line)
+      at=index(line,"@")
+      if (at>0) {
+        creds=substr(line,1,at-1)
+        colon=index(creds,":")
+        if (colon>0) {
+          user=substr(creds,1,colon-1)
+          pass=substr(creds,colon+1)
+          if (index(pass,user)==1) { ok=0 }
+        }
+      }
+    }
+    END{ if (!ok) { print "Password appears prefixed by username"; exit 1 } }
+  ' "$app_yml"
 }
 
-  # Write per-key SMTP entries (address/port/username/password)
-  # (legacy: performed via sed substitutions)
-  # NOTE: historically fragile with special chars
-  update_setting_yaml "DISCOURSE_SMTP_ADDRESS"  "$smtp_address"
-  update_setting_yaml "DISCOURSE_SMTP_PORT"     "$smtp_port"
-  update_setting_yaml "DISCOURSE_SMTP_USER_NAME" "$smtp_user"
-  update_setting_yaml "DISCOURSE_SMTP_PASSWORD" "$smtp_password"
-}
+  # Legacy per-key writes removed in favor of atomic SMTP_URL write above.
+}

Why this meets your constraint
• Zero Ruby/Python on the host: only Bash + awk + od/printf/grep (all standard).
• No sed replacement of secrets: we avoid the “Bash → sed → YAML” multi-escape trap.
• Atomic-ish write: edit into a temp file, then mv over containers/app.yml.
• Backwards-safe: if env: doesn’t exist, we create it minimally; if it does, we update in place and remove conflicting DISCOURSE_SMTP_* lines.
• Credentials are URL-encoded before insertion, so special chars in user/pass won’t break the URL parser in Rails/Net::SMTP.

If you’d prefer to keep the per-key variables instead of SMTP_URL, I can send a sibling patch that (still without sed) uses the same awk strategy to set:

DISCOURSE_SMTP_ADDRESS
DISCOURSE_SMTP_PORT
DISCOURSE_SMTP_USER_NAME
DISCOURSE_SMTP_PASSWORD

…with YAML-safe double-quoted values and anchored replacements.

Then I can’t imagine how that happened. Can you replicate it as in my example?

Did you copy/paste all of the text in a single batch so that the buffer couldn’t keep up with how fast you were pasting?

hmmm ik vraag me af of we de logica gewoon in launcher2 kunnen bundelen, dan kunnen we die gebruiken om de installatie uit te voeren.
De hoeveelheid horden waar discourse-setup overheen springt is spectaculair.

Is het mogelijk om de launcher te compileren tot een uitvoerbaar binair bestand voor release? Op deze manier kan de launcher (bash) alleen het bijbehorende uitvoerbare bestand (binair) downloaden op basis van het systeem.

1 like