./discourse-setup pone el nombre de usuario SMTP al principio de la contraseña SMTP

Entorno

  • Discourse: latest tests-passed
  • Sistema operativo anfitrión: [Ubuntu 24.04 LTS / 24.04.3 LTS]
  • Método de instalación: Instalación oficial de Docker, ejecutando ./discourse-setup
  • Plataforma: VPS (Digital Ocean)

Pasos para reproducir

  1. Ejecute ./discourse-setup desde /var/discourse
  2. Ingrese los detalles de SMTP cuando se le solicite:
  1. Finalice la configuración e inspeccione containers/app.yml

Resultado esperado

app.yml debería contener dos campos distintos, por ejemplo:

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

Resultado real

El campo de la contraseña se escribe con el nombre de usuario antepuesto:

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

Esto provoca que la entrega de correo falle, y discourse-doctor confirma la contraseña manipulada.

Notas
• Poner la contraseña entre comillas explícitamente en el aviso no cambia el resultado.
• La contraseña contiene caracteres especiales (@, !), pero el problema no es solo el entrecomillado de YAML: la cadena del nombre de usuario se concatena literalmente al principio de la contraseña.
• Reproducible en múltiples ejecuciones de ./discourse-setup.

1 me gusta

¿Puedes probar si esto se relaciona con letras específicas en la contraseña, tal vez @ / ! ¿se expone el error?

Todos los scripts de sed/awk y los scripts de bash avanzados en discourse-setup hacen que el mantenimiento sea bastante complicado. ¿Quizás @pfaffman tenga algunas ideas al respecto?

1 me gusta

De acuerdo. Esa es una buena apuesta.

@Ethsim2 si cambias la contraseña a algo con solo números y letras y que no sea igual a parte del nombre de usuario, ¿tendrás este problema?

¿Puedes compartir los valores que estás utilizando?

Intentaré echar un vistazo más tarde hoy.

Si quitaste la parte 83f…com de la contraseña y solo dejaste la contraseña (5AH…), ¿funciona?

2 Me gusta

sí, luego ejecuto .\launcher rebuild app, ya que .\discourse-setup ya había configurado SWAP

Sí. Ese es el problema. Hay múltiples niveles de escape que deben ocurrir, como cuando bash lee el valor, cuando bash entrega el valor a sed, cuando sed lo reemplaza y luego, tal vez, cuando el archivo yml lo recibe. Es un problema conocido:

He vuelto a categorizar esto como #soporte.

Los nombres de usuario de SMTP siempre contienen @, ¿no?

Sí. No creo que @ deba causar un problema.

1 me gusta

no hay ningún ! en mi captura de pantalla donde ocurre la concatenación

¿Cuál es la contraseña que intentas introducir?

la contraseña en la captura de pantalla es 5AHQXrf4LDUmRB1J :slightly_smiling_face:

Editar: esta contraseña es una contraseña predeterminada de Brevo creada en una cuenta que ya ha sido eliminada.

No puedo replicarlo. ¿Qué sistema operativo estás usando?

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)

Digital Ocean ubuntu 24.04

Recomendaciones generadas por LLM/IA

\nEl siguiente es un parche ajustado que:\n\n- deja de usar sed\n- construye una SMTP_URL codificada en porcentaje\n- edita containers/app.yml usando YAML de Ruby (Psych), por lo que el escape/codificación de YAML es manejado por un analizador real\n- elimina las variables SMTP por clave para evitar contradicciones\n\naplicar con git apply -p0 en el repositorio discourse_docker.\n\n- - -\n\nParche 1 -\n\ndiscourse-setup\n\n(escribe SMTP usando YAML de Ruby, no 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+ # Construye una SMTP_URL codificada en URL usando la biblioteca estándar de Python (sin juegos de escape de shell)\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+ # IMPORTANTE: lee variables sin manipulación de barras invertidas\n+ # (estas provienen de prompts anteriores; solo asegúrate de que -r se use en el momento del prompt)\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+ # Usa Ruby para cargar/modificar/guardar YAML de forma segura (elimina 3 capas de escape)\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+# Asegura que la estructura de nivel superior sea un Hash y tenga env\n+unless doc.is_a?(Hash)\n+ abort \"containers/app.yml no se analiza como un Hash\"\n+end\n+doc[\"env\"] ||= {}\n+env = doc[\"env\"]\n+\n+# Escribe SMTP_URL en una sola línea; elimina las variables por clave para evitar conflictos\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+# Guarda de nuevo. (Psych preserva las cadenas de forma segura entre comillas según sea necesario.)\n+File.write(path, Psych.dump(doc))\n+RUBY\n+\n+ # chequeo rápido de cordura para el clásico fallo de \"contraseña prefijada por nombre de usuario\"\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 missing after write\"\n+creds = m.group(1).split('@',1)[0].split('://',1)[-1]\n+assert \":\" in creds, \"SMTP_URL creds missing ':'\"\n+u, p = creds.split(':',1)\n+assert not p.startswith(u), \"Password appears prefixed by username\"\n+print(\"SMTP_URL looks sane.\")\n+PY\n+}\n+\n- # Escribe entradas SMTP por clave (dirección/puerto/nombre de usuario/contraseña)\n- # (legado: realizado a través de sustituciones de sed)\n- # NOTA: históricamente frágil con caracteres especiales\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+ # (escrituras legadas por clave eliminadas en favor de SMTP_URL a través de YAML)\n+}\n\n\nluego Parche 2 -\n\ntemplates/web.template.yml\n\n(para documentar la ruta más segura)\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+ ## Configuración SMTP preferida en una sola línea (establecida por discourse-setup):\n+ ## Codifica en URL el nombre de usuario y la contraseña; ejemplo:\n+ ## SMTP_URL: \"smtp://user%40example.com:p%40ss%3Aword@smtp.example.com:587\"\n+ ##\n+ #SMTP_URL:\n+\n ## Si no puedes usar SMTP_URL, puedes establecer variables por clave en su lugar.\n ## Ten cuidado, ya que editar esas líneas con herramientas de shell puede ser frágil si los valores incluyen\n ## caracteres como @, :, /, \", \\ o saltos de línea.\n\n\npor qué funciona (y qué evita)\n\t•\tCapa bash: solo interpolamos variables simples; los secretos se pasan a Python/Ruby a través de stdin/argv, no a través de expresiones regulares de sed o evaluaciones de shell.\n\t•\tCapa sed: eliminada por completo.\n\t•\tCapa YAML: Ruby/Psych maneja las comillas y el escape correctamente; sin comillas hechas a mano.\n\t•\tCredenciales SMTP: la codificación % en SMTP_URL es el lugar correcto para codificar caracteres especiales para la autenticación.\n\nsi prefieres mantener las variables por clave, puedo darte un parche hermano que usa el mismo enfoque Ruby-YAML para establecer DISCOURSE_SMTP_* directamente (todavía sin sed), pero la ruta SMTP_URL es la más limpia porque es una clave, una escritura, un paso de codificación.\n

Lamentablemente, no podemos depender de que Ruby esté instalado en el sistema anfitrión en las instalaciones de Discourse; sería relativamente sencillo que todo el script fuera de Ruby si pudiéramos, pero no hay garantía.

2 Me gusta
también generado por LLM/IA

Entendido: no hay Ruby/Python en el host. Aquí tienes un parche puro de Bash + awk que:
• construye una SMTP_URL codificada en porcentaje utilizando solo Bash (bucle byte a byte; sin Python),
• inserta o reemplaza SMTP_URL bajo el bloque env: usando awk (sin sed),
• elimina las líneas DISCOURSE_SMTP_* por clave (eliminaciones ancladas seguras),
• añade una pequeña verificación de cordura utilizando solo grep/awk.

Aplícalo con git apply -p0 en el repositorio discourse_docker.

--- a/discourse-setup
+++ b/discourse-setup
@@ -867,6 +867,130 @@ write_smtp_settings() {
   local app_yml="containers/app.yml"
   [[ -f "$app_yml" ]] || die "No se puede encontrar $app_yml. ¿Ejecutaste bootstrap?"

+  ##############################################
+  # Codificador de URL puro de Bash para credenciales SMTP #
+  ##############################################
+  # Codifica todo excepto A-Z a-z 0-9 . _ ~ -
+  # Funciona byte a byte; requiere bash y printf.
+  urlencode_cred() {
+    local s="$1" out= i ch o
+    # establece la configuración regional C para obtener semántica de bytes
+    LC_ALL=C
+    for ((i=0; i<${#s}; i++)); do
+      ch="${s:i:1}"
+      case "$ch" in
+        [A-Za-z0-9._~-])
+          out+="$ch"
+          ;;
+        *)
+          # Obtiene el valor del byte: imprime el carácter, lee con od, luego formatea %HH
+          # Evita dependencias pesadas externas; od está en coreutils / busybox.
+          o=$(printf '%s' "$ch" | od -An -tu1 | awk '{$1=$1;print $1}')
+          # Si od falló de alguna manera (vacío), recurre a hexadecimal a través de printf %02X del primer 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"
+  }
+
+  # Construye SMTP_URL (una sola línea) a partir de las respuestas recopiladas
+  # Estas variables se recopilan anteriormente en 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}"
+
+  ########################################################
+  # Edición segura para YAML mediante awk (sin sed / sin tiempos de ejecución externos)
+  # - asegura que exista env:
+  # - inserta o reemplaza SMTP_URL bajo env:
+  # - elimina las claves DISCOURSE_SMTP_*
+  ########################################################
+  awk -v NEWVAL="$smtp_url" '
+    BEGIN{
+      have_env=0; in_env=0; inserted=0
+    }
+    # detecta la línea env: en el nivel superior (inicio de línea, posiblemente con sangría 0..)
+    # consideraremos una sangría de dos espacios para los hijos.
+    /^[[:space:]]*env:[[:space:]]*$/ {
+      print; have_env=1; in_env=1; next
+    }
+    # saliendo del bloque env: cuando la sangría vuelve a 0 o a la siguiente clave de nivel superior
+    in_env && /^[^[:space:]]/ {
+      if (!inserted) {
+        print "  SMTP_URL: \"' NEWVAL "'\""
+        inserted=1
+      }
+      in_env=0
+    }
+    # mientras esté en env:, maneja reemplazos y eliminaciones
+    in_env {
+      # descarta líneas DISCOURSE_SMTP_* por clave por completo
+      if ($0 ~ /^[[:space:]]*DISCOURSE_SMTP_(ADDRESS|PORT|USER_NAME|PASSWORD):/) next
+      # reemplaza la línea SMTP_URL existente
+      if ($0 ~ /^[[:space:]]*SMTP_URL:[[:space:]]*/) {
+        print "  SMTP_URL: \"' NEWVAL "'\""
+        inserted=1
+        next
+      }
+      print
+      next
+    }
+    { print }
+    END{
+      # Si env: nunca existió, agrégalo con la clave
+      if (!have_env) {
+        print ""
+        print "env:"
+        print "  SMTP_URL: \"' NEWVAL "'\""
+      } else if (in_env && !inserted) {
+        # env: existió y todavía estábamos en él al final del archivo
+        print "  SMTP_URL: \"' NEWVAL "'\""
+      }
+    }
+  ' "$app_yml" > "$app_yml.tmp.$$" && mv "$app_yml.tmp.$$" "$app_yml"
+
+  ##############################################
+  # Verificación de cordura: guardia básica contra la manipulación #
+  ##############################################
+  # 1) SMTP_URL presente
+  grep -q '^[[:space:]]*SMTP_URL:' "$app_yml" || die "SMTP_URL no escrito en $app_yml"
+  # 2) la contraseña no está precedida por el nombre de usuario (firma de fallo clásica)
+  awk '
+    BEGIN{ok=1}
+    /^[[:space:]]*SMTP_URL:[[:space:]]*"/ {
+      line=$0
+      gsub(/^[[:space:]]*SMTP_URL:[[:space:]]*"/,"",line)
+      gsub(/".*$/,"",line)
+      # Extrae las credenciales antes de @ y después del esquema
+      # por ejemplo, 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 "La contraseña parece estar precedida por el nombre de usuario"; exit 1 } }
+  ' "$app_yml"
+ }
+
   ##############################################
   # Pure-Bash URL encoder for SMTP credentials #
   ##############################################
@@ -997,17 +1121,4 @@
   # Legacy per-key writes removed in favor of atomic SMTP_URL write above.
 }
  • 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”

-}


Por qué esto cumple tu restricción
	•	Cero Ruby/Python en el host: solo Bash + awk + od/printf/grep (todos estándar).
	•	Sin reemplazo de `sed` de secretos: evitamos la trampa de múltiples escapes "Bash → sed → YAML".
	•	Escritura atómica (aproximadamente): edita en un archivo temporal, luego `mv` sobre `containers/app.yml`.
	•	Seguro hacia atrás: si `env:` no existe, lo creamos mínimamente; si existe, lo actualizamos en el lugar y eliminamos las líneas conflictivas `DISCOURSE_SMTP_*`.
	•	Las credenciales se codifican en URL antes de insertarlas, por lo que los caracteres especiales en el usuario/contraseña no romperán el analizador de URL en Rails/Net::SMTP.

Si prefieres mantener las variables por clave en lugar de `SMTP_URL`, puedo enviar un parche hermano que (aún sin `sed`) utiliza la misma estrategia de `awk` para establecer:

DISCOURSE_SMTP_ADDRESS
DISCOURSE_SMTP_PORT
DISCOURSE_SMTP_USER_NAME
DISCOURSE_SMTP_PASSWORD


…con valores entre comillas dobles seguros para YAML y reemplazos anclados.

Entonces no me imagino cómo sucedió eso. ¿Puedes replicarlo como en mi ejemplo?

¿Copiastes y pegastes todo el texto en un solo lote para que el búfer no pudiera seguir el ritmo de la velocidad a la que pegabas?

hmmm me pregunto si podemos simplemente agrupar la lógica en launcher2 y luego podemos usarla para ejecutar la configuración.
La cantidad de obstáculos que está superando discourse-setup es espectacular.

¿Es posible compilar el lanzador en un archivo ejecutable binario para su lanzamiento? De esta manera, el lanzador (bash) solo puede descargar el archivo ejecutable (binario) correspondiente según el sistema.

1 me gusta