Über unsLeistungenProjekteBlogQualifikationenKontakt
← Zurück zum Blog Azure & Cloud

E-Mails aus einer Anwendung senden, ohne ein einziges Passwort

Microsoft hat SMTP mit Passwort abgeschaltet. Die Antwort ist nicht, ein besseres Geheimnis zu speichern: Es ist, gar kein Geheimnis zu speichern. Verwaltete Identität, Microsoft Graph, und Mail.Send an eine Gruppe von Postfächern gebunden. Hier ist die ganze Konfiguration, Fallen inbegriffen.

pH7x Systems® · · 5 Min. Lesezeit

Eine Anwendung muss eine E-Mail senden. Eine Bestätigung, eine Warnung, eine Quittung. Die erste Lösung, die einem einfällt, ist die falsche: ein Postfach, ein Passwort, und dieses Passwort in eine Umgebungsvariable gelegt.

Am ersten Tag funktioniert es. Und es ist eine Bombe mit Zeitzünder: Microsoft schaltet SMTP mit Passwort ab, das Geheimnis entweicht mit dem Code oder mit dem Backup, und wer es aufgreift, sendet im Namen des Unternehmens. Es muss nicht so sein: Eine Anwendung kann E-Mails senden, ohne ein einziges Passwort zu speichern. Hier ist die ganze Konfiguration, von Anfang bis Ende, mit den Fallen, die uns unterwegs einen Vormittag gekostet haben.

Das Problem: das Passwort, das nicht existieren sollte

Der alte Weg ist leicht zu schreiben und schwer zu verteidigen. Das Geheimnis lebt irgendwo, und irgendwo ist immer ein Ort zu viel.

ts
// Don't do this: the password lives with the code, and leaks with it.
const user = "noreply@company.com";
const pass = process.env.SMTP_PASSWORD;   // a secret that someone has to keep
await smtp.send({ user, pass, to, subject, html });

Darunter liegt ein größeres Problem. Microsoft deaktiviert SMTP AUTH standardmäßig in neuen Tenants und entfernt es aus den alten. Der Code oben hört ohne Vorwarnung auf zu funktionieren, und die instinktive Reaktion, SMTP AUTH wieder einzuschalten, öffnet genau die Tür, die Microsoft schließt.

Der richtige Weg: Identität statt Geheimnis

Die Anwendung läuft in Azure, und Azure weiß, wer sie ist. Das nennt sich verwaltete Identität: Die Plattform gibt der Anwendung auf Anfrage ein Token, ohne dass irgendwo ein Passwort existiert. Es gibt kein Geheimnis zu speichern, zu rotieren oder entweichen zu lassen.

Die E-Mail geht über Microsoft Graph, nicht über SMTP. Microsoft Graph akzeptiert dieses Token, prüft, dass die Identität berechtigt ist, und stellt zu. Die gesamte Authentifizierung wird zum Problem der Plattform und ist keine Datei mehr von uns.

Der Code

Es sind weniger Zeilen als die Version mit Passwort, und es fehlt das Feld, das einen Angreifer interessiert. DefaultAzureCredential kümmert sich um das Token: in der Cloud nutzt es die verwaltete Identität, auf unserer Maschine die az login-Sitzung.

ts
import { DefaultAzureCredential } from "@azure/identity";

const credential = new DefaultAzureCredential();
const SENDER = "hello@company.com";

async function send(to: string, subject: string, html: string): Promise<void> {
  const token = await credential.getToken("https://graph.microsoft.com/.default");
  if (!token) throw new Error("no token for Microsoft Graph");

  const res = await fetch(
    `https://graph.microsoft.com/v1.0/users/${encodeURIComponent(SENDER)}/sendMail`,
    {
      method: "POST",
      headers: {
        Authorization: `Bearer ${token.token}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        message: {
          subject,
          body: { contentType: "HTML", content: html },
          toRecipients: [{ emailAddress: { address: to } }],
        },
        saveToSentItems: true,
      }),
    },
  );

  if (!res.ok) throw new Error(`sendMail ${res.status}: ${await res.text()}`);
}

Derselbe Code läuft an beiden Orten, und an keinem ist ein Passwort niedergeschrieben. Es fehlt der weniger offensichtliche Teil: Exchange zu sagen, dass diese Identität nur aus einem Postfach senden kann, und aus keinem anderen.

Den Versand an eine Gruppe von Postfächern binden

Die Microsoft Graph Mail.Send-Berechtigung, allein erteilt, lässt die Anwendung im Namen jedes Postfachs im Tenant senden, das des Administrators eingeschlossen. Die Einschränkung erfolgt in Exchange, mit RBAC für Anwendungen. Und der Geltungsbereich sollte kein festes Postfach sein, sondern eine Gruppe: so fügen wir später Postfächer hinzu oder entfernen sie, ohne die Zuweisung erneut anzufassen.

powershell
# A mail-enabled security group is the scope: add or remove mailboxes later,
# without ever touching the role assignment again.
New-DistributionGroup -Name "api-senders" -Type Security `
  -PrimarySmtpAddress "api-senders@company.com"

Add-DistributionGroupMember -Identity "api-senders" -Member "hello@company.com"

Mit der erstellten Gruppe registrieren wir die Identität in Exchange, definieren einen Geltungsbereich, der auf die Mitglieder der Gruppe auflöst, und erteilen Mail.Send, an diesen Bereich gebunden.

powershell
# Register the managed identity in Exchange. Use the AppId, never the ObjectId.
New-ServicePrincipal -AppId $appId -ObjectId $objectId -DisplayName "api"

# A scope that resolves to the group's members, and to nobody else.
New-ManagementScope -Name "only-api-senders" `
  -RecipientRestrictionFilter "MemberOfGroup -eq '$groupDn'"

# Grant Mail.Send, restricted to that scope.
New-ManagementRoleAssignment -App $servicePrincipalId `
  -Role "Application Mail.Send" -CustomResourceScope "only-api-senders"

Die AppId ist Pflicht, nicht die ObjectId. Sie zu vertauschen gibt einen 403 «Blocked by tenant configured AppOnly AccessPolicy settings», einen Fehler, der nicht sagt, was falsch ist, und uns an der falschen Stelle suchen lässt.

Die Fallen, die die ganze Sicherheit kosten

Die erste ist schon aufgetaucht: AppId, nie ObjectId. Die zweite ist schlimmer, denn die Konfiguration sieht richtig aus und ist es nicht. Die Berechtigungen von Entra und Exchange-RBAC addieren sich. Hat die Identität auch ein Mail.Send in Entra erteilt, ohne Geltungsbereich, hebt die Vereinigung der beiden die Einschränkung auf, und die Anwendung kann wieder als jedes Postfach senden.

Die Regel ist eine einzige: Mail.Send existiert nur im Exchange-RBAC, auf die Gruppe beschränkt. In Entra, null. Ist die Zustimmung dort, entfernt man sie, sonst zählt die ganze Arbeit in Exchange nichts.

Die Verteilung braucht Zeit

Die Konfiguration erledigt, testeten wir, und es schlug fehl. Wir gingen zurück, prüften alles, es war richtig, und es schlug weiter fehl. Das Problem war nicht die Konfiguration: es war die Zeit. RBAC-Änderungen in Exchange Online sind nicht sofort wirksam, sie replizieren über den Dienst und können bis zu einer halben Stunde brauchen, um überall zu gelten.

Die Lektion kostete uns den Vormittag: nach einer Änderung am Geltungsbereich oder an der Zuweisung warten, bevor man schließt, es sei falsch. Test-ServicePrincipalAuthorization selbst kann während dieses Fensters mit dem alten Stand antworten. Mehr Dinge in diesem Intervall zu ändern ist der beste Weg, das zu zerbrechen, was schon richtig war.

Wie man beweist, dass es verriegelt ist

Eine Einschränkung, die man nicht nachweist, zählt nicht. Exchange beantwortet die Frage direkt, Postfach für Postfach, und das ist der Beweis, den wir aufbewahren.

powershell
Test-ServicePrincipalAuthorization -Identity "api" -Resource hello@company.com
# InScope : True    -> can send as hello@, which is what we want

Test-ServicePrincipalAuthorization -Identity "api" -Resource ceo@company.com
# InScope : False   -> cannot, and that is exactly why the CEO mailbox is safe

Was bleibt

Eine Anwendung, die E-Mails sendet, ohne irgendwo ein Passwort, und die nur aus der Gruppe senden kann, die wir ihr gegeben haben. Wird sie morgen kompromittiert, findet der Angreifer kein Geheimnis zum Stehlen und schreibt in niemandes anderem Namen. Es ist keine Kanone, um eine Fliege zu töten: Es ist die kleinste Berechtigung, die das Problem löst, und der Beweis, dass sie wirklich die kleinste ist.

Weiterlesen