Über unsLeistungenProjekteR&DBlogAnfangenKontakt
← 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, Fehler 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 Fehlern, 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 zwei Fehler, 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

Microsoft 365 & SharePoint

Der Stand von SharePoint 2026: was vorbei ist, was aktuell ist und worauf man baut

Jedes Jahr erklärt jemand SharePoint für tot. Das war es nie. Doch binnen Monaten erreichten echte Teile ihr Ende: das Add-in-Modell und Azure ACS wurden im April eingestellt, und SharePoint Server 2016 und 2019, InfoPath und SharePoint Designer verließen im Juli den Support. Hier steht, was wirklich endete, was es ersetzte und worauf man jetzt baut, on-premises und in der Cloud.

·6 Min. Lesezeit
Microsoft 365 & SharePoint

Windows 10 ist am Ende: Windows 11 ist das Minimum, nicht der Plan

Der Support endete im Oktober 2025. Ob ein Gerätebestand schon auf Windows 11 läuft, für Extended Security Updates zahlt oder schlicht ungeschützt ist, die Entscheidung darunter ist dieselbe: die PCs tauschen, oder den erzwungenen Umzug nutzen, um den Arbeitsplatz dahinter zu modernisieren. Das ist das Wie, mit den Befehlen für Intune, Autopilot und Azure, und der Grund, warum dasselbe Windows 10 in der Cloud kostenlos am Leben bleibt.

·7 Min. Lesezeit
Microsoft 365 & SharePoint

SPFx 1.23.2: aktualisieren heißt nicht, eine Nummer zu ändern

Die vorige Version wurde wegen Regressionen aus npm zurückgezogen, und das ist die Warnung: Das Risiko einer SharePoint-Framework-Aktualisierung liegt fast nie in der SharePoint-Laufzeit, es liegt im Build. Was sich in Heft, in Sass und in npm audit geändert hat, mit dem Code, der zeigt, wie es war, wie es ist, und wie es sein wird.

·6 Min. Lesezeit