Freigeben über


Konfigurieren der gegenseitigen TLS-Authentifizierung in Azure App Service

Sie können den Zugriff auf Ihre Azure App Service-App einschränken, indem Sie verschiedene Authentifizierungstypen für die App aktivieren. Eine Möglichkeit zum Einrichten der Authentifizierung besteht darin, ein Clientzertifikat anzufordern, wenn die Clientanforderung mithilfe von TLS (Transport Layer Security) / Secure Sockets Layer (SSL) gesendet wird und um das Zertifikat zu überprüfen. Dieser Mechanismus wird als gegenseitige Authentifizierung oder Clientzertifikatauthentifizierung bezeichnet. In diesem Artikel wird gezeigt, wie Sie Ihre App für die Verwendung der Clientzertifikatauthentifizierung einrichten.

Hinweis

Ihr App-Code muss das Clientzertifikat überprüfen. Der App-Dienst macht mit dem Clientzertifikat nichts anderes, als es an Ihre App weiterzuleiten.

Wenn Sie auf Ihre Website über HTTP und nicht AUF HTTPS zugreifen, erhalten Sie keine Clientzertifikate. Wenn Ihre Anwendung Clientzertifikate erfordert, sollten Sie Anforderungen an Ihre Anwendung nicht über HTTP zulassen.

Vorbereiten Ihrer Web-App

Wenn Sie benutzerdefinierte TLS/SSL-Bindungen erstellen oder Clientzertifikate für Ihre App Service-App aktivieren möchten, muss sich Ihr App Service-Plan in den Ebenen "Basic", "Standard", "Premium" oder "Isolated" befinden.

So stellen Sie sicher, dass sich Ihre Web-App in einer unterstützten Preisstufe befindet:

Wechseln zur Web-App

  1. Geben Sie im Suchfeld des Azure-PortalsApp Services ein, und wählen Sie sie dann in den Suchergebnissen aus.

  2. Wählen Sie auf der Seite "App Services " Ihre Web-App aus:

    Screenshot der Seite

    Sie befinden sich jetzt auf der Verwaltungsseite Ihrer Web-App.

Überprüfen des Tarifs

  1. Wählen Sie im linken Menü für Ihre Web-App unter "Einstellungen" die Option "Skalieren" (App Service-Plan) aus.

  2. Stellen Sie sicher, dass sich Ihre Web-App nicht auf der F1- oder D1-Ebene befindet. Diese Ebenen unterstützen keine benutzerdefinierte TLS/SSL.

  3. Wenn Sie Ihren App Service-Plan hochskalieren müssen, führen Sie die Schritte im nächsten Abschnitt aus. Schließen Sie andernfalls den Bereich " Hochskalieren ", und überspringen Sie den nächsten Abschnitt.

Hochskalieren Ihres App Service-Plans

  1. Wählen Sie einen kostenpflichtigen Tarif aus, beispielsweise B1, B2, B3 oder einen beliebigen Tarif aus der Kategorie Produktion.

  2. Wählen Sie Auswählen aus, wenn Sie fertig sind.

    Nach Abschluss des Skalierungsvorgangs wird eine Meldung angezeigt, die besagt, dass der Plan aktualisiert wurde.

Aktivieren von Clientzertifikaten

Wenn Sie Clientzertifikate für Ihre App aktivieren, sollten Sie den Clientzertifikatmodus auswählen. Der Modus definiert, wie Ihre App eingehende Clientzertifikate verarbeitet. Die Modi werden in der folgenden Tabelle beschrieben:

Clientzertifikatmodus Beschreibung
Erforderlich Für alle Anforderungen ist ein Clientzertifikat erforderlich.
Wahlfrei Anfragen können ein Clientzertifikat verwenden. Clients werden standardmäßig zur Eingabe eines Zertifikats aufgefordert. Browserclients zeigen z. B. eine Aufforderung zum Auswählen eines Zertifikats für die Authentifizierung an.
Optionaler interaktiver Benutzer Anfragen können ein Clientzertifikat verwenden. Clients werden standardmäßig nicht zur Eingabe eines Zertifikats aufgefordert. Browserclients zeigen z. B. keine Aufforderung zum Auswählen eines Zertifikats für die Authentifizierung an.

So verwenden Sie das Azure-Portal, um Clientzertifikate zu aktivieren:

  1. Wechseln Sie zu Ihrer App-Verwaltungsseite.
  2. Wählen Sie im linken Menü " Allgemeine Konfigurationseinstellungen>" aus.
  3. Wählen Sie für den Clientzertifikatmodus Ihre Wahl aus.
  4. Wählen Sie Speichern aus.

Ausschließen von Pfaden von der Anforderung der Authentifizierung

Wenn Sie die gegenseitige Authentifizierung für Ihre Anwendung aktivieren, benötigen alle Pfade unter dem Stammverzeichnis Ihrer App ein Clientzertifikat für den Zugriff. Um diese Anforderung für bestimmte Pfade aufzuheben, definieren Sie in Ihrer Anwendungskonfiguration Ausschlusspfade.

Hinweis

Die Verwendung eines Clientzertifikatausschlusspfads löst die TLS-Neuverhandlung für eingehende Anforderungen an die App aus.

  1. Wählen Sie im linken Menü der App-Verwaltungsseite Einstellungen>Konfiguration aus. Wählen Sie die Registerkarte Allgemeine Einstellungen aus.

  2. Wählen Sie neben Zertifikatausschlusspfaden das Bleistiftsymbol aus.

  3. Wählen Sie "Neuer Pfad" aus, geben Sie einen Pfad oder eine Liste von Pfaden an, die durch oder ,getrennt sind;, und wählen Sie dann "OK" aus.

  4. Wählen Sie Speichern aus.

Der folgende Screenshot zeigt, wie Sie einen Zertifikatausschlusspfad festlegen. In diesem Beispiel fordert kein Pfad der App, der mit /public beginnt, ein Clientzertifikat an. Beim Pfadabgleich wird die Groß-/Kleinschreibung nicht berücksichtigt.

Screenshot, der zeigt, wie Sie einen Zertifikatausschlusspfad festlegen.

Clientzertifikat und TLS-Aushandlung

Für einige Clientzertifikateinstellungen muss für App Service eine TLS-Neuverhandlung eine Anforderung lesen, bevor bekannt ist, ob ein Clientzertifikat angefordert werden soll. Beide der folgenden Einstellungen lösen die TLS-Neuverhandlung aus:

Hinweis

TLS 1.3 und HTTP 2.0 unterstützen keine TLS-Neuverhandlung. Diese Protokolle funktionieren nicht, wenn Ihre App mit Clientzertifikateinstellungen konfiguriert ist, die TLS-Neuverhandlung verwenden.

Um die TLS-Neuverhandlung zu deaktivieren und die App Clientzertifikate während des TLS-Handshake aushandeln zu lassen, müssen Sie die folgenden Aktionen in Ihrer App ausführen:

  • Legen Sie den Clientzertifikatmodus auf "Erforderlich " oder "Optional" fest.
  • Entfernen Sie alle Clientzertifikat-Ausschlusspfade.

Große Dateien mit TLS-Neuverhandlung hochladen

Clientzertifikatkonfigurationen, die TLS-Neuverhandlung verwenden, können eingehende Anforderungen mit Dateien, die größer als 100 KB sind, nicht unterstützen. Dieser Grenzwert wird durch Puffergrößenbeschränkungen verursacht. In diesem Szenario schlagen alle POST- oder PUT-Anforderungen, die über 100 KB liegen, mit einem 403-Fehler fehl. Dieser Grenzwert ist nicht konfigurierbar und kann nicht erhöht werden.

Um den Grenzwert von 100 KB zu beheben, sollten Sie die folgenden Lösungen in Betracht ziehen:

  • Deaktivieren Sie die TLS-Neuverhandlung. Führen Sie die folgenden Aktionen in den Clientzertifikatkonfigurationen Ihrer App aus:
    • Legen Sie den Clientzertifikatmodus auf "Erforderlich " oder "Optional" fest.
    • Entfernen Sie alle Clientzertifikat-Ausschlusspfade.
  • Senden Sie vor der PUT/POST-Anforderung eine HEAD-Anforderung. Die HEAD-Anforderung verwaltet das Clientzertifikat.
  • Fügen Sie Ihrer Anforderung den Expect: 100-Continue-Header hinzu. Dieser Header bewirkt, dass der Client wartet, bis der Server mit 100 Continue antwortet, bevor der Anforderungstext gesendet wird, und die Puffer werden umgangen.

Zugreifen auf das Clientzertifikat

In App Service erfolgt die TLS-Beendigung der Anforderung beim Front-End-Lastenausgleich. Wenn App Service die Anforderung an Ihren App-Code mit aktivierten Clientzertifikaten weiterleitet, wird ein X-ARR-ClientCert-Anforderungsheader mit dem Clientzertifikat eingefügt. Der App Service macht mit diesem Clientzertifikat nichts anderes, als es an Ihre App weiterzuleiten. Ihr App-Code muss das Clientzertifikat überprüfen.

In ASP.NET ist das Clientzertifikat über die HttpRequest.ClientCertificate Eigenschaft verfügbar.

In anderen Anwendungsstapeln (Node.js, PHP) ist das Clientzertifikat über einen Base64-codierten Wert im X-ARR-ClientCert Anforderungsheader verfügbar.

ASP.NET Core-Beispiel

Für ASP.NET Core ist Middleware verfügbar, um weitergeleitete Zertifikate zu analysieren. Separate Middleware ist für die Nutzung der weitergeleiteten Protokollheader verfügbar. Beides muss vorhanden sein, damit weitergeleitete Zertifikate akzeptiert werden können. Sie können benutzerdefinierte Zertifikatüberprüfungslogik in den Zertifikatauthentifizierungsoptionen platzieren:

public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddControllersWithViews();
        // Configure the application to use the protocol and client IP address forwarded by the front-end load balancer.
        services.Configure<ForwardedHeadersOptions>(options =>
        {
            options.ForwardedHeaders =
                ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
            // By default, only loopback proxies are allowed. Clear that restriction to enable this explicit configuration.
            options.KnownNetworks.Clear();
            options.KnownProxies.Clear();
        });       
        
        // Configure the application to use the client certificate forwarded by the front-end load balancer.
        services.AddCertificateForwarding(options => { options.CertificateHeader = "X-ARR-ClientCert"; });

        // Add certificate authentication so that when authorization is performed the user will be created from the certificate.
        services.AddAuthentication(CertificateAuthenticationDefaults.AuthenticationScheme).AddCertificate();
    }

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }
        else
        {
            app.UseExceptionHandler("/Home/Error");
            app.UseHsts();
        }
        
        app.UseForwardedHeaders();
        app.UseCertificateForwarding();
        app.UseHttpsRedirection();

        app.UseAuthentication()
        app.UseAuthorization();

        app.UseStaticFiles();

        app.UseRouting();
        
        app.UseEndpoints(endpoints =>
        {
            endpoints.MapControllerRoute(
                name: "default",
                pattern: "{controller=Home}/{action=Index}/{id?}");
        });
    }
}

Beispiel für ASP.NET Webformulare

    using System;
    using System.Collections.Specialized;
    using System.Security.Cryptography.X509Certificates;
    using System.Web;

    namespace ClientCertificateUsageSample
    {
        public partial class Cert : System.Web.UI.Page
        {
            public string certHeader = "";
            public string errorString = "";
            private X509Certificate2 certificate = null;
            public string certThumbprint = "";
            public string certSubject = "";
            public string certIssuer = "";
            public string certSignatureAlg = "";
            public string certIssueDate = "";
            public string certExpiryDate = "";
            public bool isValidCert = false;

            //
            // Read the certificate from the header into an X509Certificate2 object.
            // Display properties of the certificate on the page.
            //
            protected void Page_Load(object sender, EventArgs e)
            {
                NameValueCollection headers = base.Request.Headers;
                certHeader = headers["X-ARR-ClientCert"];
                if (!String.IsNullOrEmpty(certHeader))
                {
                    try
                    {
                        byte[] clientCertBytes = Convert.FromBase64String(certHeader);
                        certificate = new X509Certificate2(clientCertBytes);
                        certSubject = certificate.Subject;
                        certIssuer = certificate.Issuer;
                        certThumbprint = certificate.Thumbprint;
                        certSignatureAlg = certificate.SignatureAlgorithm.FriendlyName;
                        certIssueDate = certificate.NotBefore.ToShortDateString() + " " + certificate.NotBefore.ToShortTimeString();
                        certExpiryDate = certificate.NotAfter.ToShortDateString() + " " + certificate.NotAfter.ToShortTimeString();
                    }
                    catch (Exception ex)
                    {
                        errorString = ex.ToString();
                    }
                    finally 
                    {
                        isValidCert = IsValidClientCertificate();
                        if (!isValidCert) Response.StatusCode = 403;
                        else Response.StatusCode = 200;
                    }
                }
                else
                {
                    certHeader = "";
                }
            }

            //
            // This is a sample verification routine. You should modify this method to suit  your application logic and security requirements. 
            // 
            //
            private bool IsValidClientCertificate()
            {
                // In this example, the certificate is accepted as a valid certificate only if these conditions are met:
                // - The certificate isn't expired and is active for the current time on the server.
                // - The subject name of the certificate has the common name nildevecc.
                // - The issuer name of the certificate has the common name nildevecc and the organization name Microsoft Corp.
                // - The thumbprint of the certificate is 30757A2E831977D8BD9C8496E4C99AB26CB9622B.
                //
                // This example doesn't test that the certificate is chained to a trusted root authority (or revoked) on the server. 
                // It allows self-signed certificates.
                //

                if (certificate == null || !String.IsNullOrEmpty(errorString)) return false;

                // 1. Check time validity of the certificate.
                if (DateTime.Compare(DateTime.Now, certificate.NotBefore) < 0 || DateTime.Compare(DateTime.Now, certificate.NotAfter) > 0) return false;

                // 2. Check the subject name of the certificate.
                bool foundSubject = false;
                string[] certSubjectData = certificate.Subject.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
                foreach (string s in certSubjectData)
                {
                    if (String.Compare(s.Trim(), "CN=nildevecc") == 0)
                    {
                        foundSubject = true;
                        break;
                    }
                }
                if (!foundSubject) return false;

                // 3. Check the issuer name of the certificate.
                bool foundIssuerCN = false, foundIssuerO = false;
                string[] certIssuerData = certificate.Issuer.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
                foreach (string s in certIssuerData)
                {
                    if (String.Compare(s.Trim(), "CN=nildevecc") == 0)
                    {
                        foundIssuerCN = true;
                        if (foundIssuerO) break;
                    }

                    if (String.Compare(s.Trim(), "O=Microsoft Corp") == 0)
                    {
                        foundIssuerO = true;
                        if (foundIssuerCN) break;
                    }
                }

                if (!foundIssuerCN || !foundIssuerO) return false;

                // 4. Check the thumbprint of the certificate.
                if (String.Compare(certificate.Thumbprint.Trim().ToUpper(), "30757A2E831977D8BD9C8496E4C99AB26CB9622B") != 0) return false;

                return true;
            }
        }
    }

Beispiel für Node.js

Der folgende Node.js Beispielcode ruft den X-ARR-ClientCert Header ab und verwendet node-forge , um die Base64-codierte Privacy Enhanced Mail (PEM)-Zeichenfolge in ein Zertifikatobjekt zu konvertieren und zu überprüfen:

import { NextFunction, Request, Response } from 'express';
import { pki, md, asn1 } from 'node-forge';

export class AuthorizationHandler {
    public static authorizeClientCertificate(req: Request, res: Response, next: NextFunction): void {
        try {
            // Get header.
            const header = req.get('X-ARR-ClientCert');
            if (!header) throw new Error('UNAUTHORIZED');

            // Convert from PEM to PKI certificate.
            const pem = `-----BEGIN CERTIFICATE-----${header}-----END CERTIFICATE-----`;
            const incomingCert: pki.Certificate = pki.certificateFromPem(pem);

            // Validate certificate thumbprint.
            const fingerPrint = md.sha1.create().update(asn1.toDer(pki.certificateToAsn1(incomingCert)).getBytes()).digest().toHex();
            if (fingerPrint.toLowerCase() !== 'abcdef1234567890abcdef1234567890abcdef12') throw new Error('UNAUTHORIZED');

            // Validate time validity.
            const currentDate = new Date();
            if (currentDate < incomingCert.validity.notBefore || currentDate > incomingCert.validity.notAfter) throw new Error('UNAUTHORIZED');

            // Validate issuer.
            if (incomingCert.issuer.hash.toLowerCase() !== 'abcdef1234567890abcdef1234567890abcdef12') throw new Error('UNAUTHORIZED');

            // Validate subject.
            if (incomingCert.subject.hash.toLowerCase() !== 'abcdef1234567890abcdef1234567890abcdef12') throw new Error('UNAUTHORIZED');

            next();
        } catch (e) {
            if (e instanceof Error && e.message === 'UNAUTHORIZED') {
                res.status(401).send();
            } else {
                next(e);
            }
        }
    }
}

Java-Beispiel

Mit der folgenden Java-Klasse wird das Zertifikat aus X-ARR-ClientCert in eine X509Certificate-Instanz codiert. certificateIsValid() überprüft, ob der Fingerabdruck des Zertifikats dem im Konstruktor angegebenen entspricht und dass das Zertifikat nicht abgelaufen ist.

import java.io.ByteArrayInputStream;
import java.security.NoSuchAlgorithmException;
import java.security.cert.*;
import java.security.MessageDigest;

import sun.security.provider.X509Factory;

import javax.xml.bind.DatatypeConverter;
import java.util.Base64;
import java.util.Date;

public class ClientCertValidator { 

    private String thumbprint;
    private X509Certificate certificate;

    /**
     * Constructor.
     * @param certificate. The certificate from the "X-ARR-ClientCert" HTTP header.
     * @param thumbprint. The thumbprint to check against.
     * @throws CertificateException if the certificate factory can't be created.
     */
    public ClientCertValidator(String certificate, String thumbprint) throws CertificateException {
        certificate = certificate
                .replaceAll(X509Factory.BEGIN_CERT, "")
                .replaceAll(X509Factory.END_CERT, "");
        CertificateFactory cf = CertificateFactory.getInstance("X.509");
        byte [] base64Bytes = Base64.getDecoder().decode(certificate);
        X509Certificate X509cert =  (X509Certificate) cf.generateCertificate(new ByteArrayInputStream(base64Bytes));

        this.setCertificate(X509cert);
        this.setThumbprint(thumbprint);
    }

    /**
     * Check that the certificate's thumbprint matches the one given in the constructor, and that the
     * certificate isn't expired.
     * @return True if the certificate's thumbprint matches and isn't expired. False otherwise.
     */
    public boolean certificateIsValid() throws NoSuchAlgorithmException, CertificateEncodingException {
        return certificateHasNotExpired() && thumbprintIsValid();
    }

    /**
     * Check certificate's timestamp.
     * @return True if the certificate isn't expired. It returns False if it is expired.
     */
    private boolean certificateHasNotExpired() {
        Date currentTime = new java.util.Date();
        try {
            this.getCertificate().checkValidity(currentTime);
        } catch (CertificateExpiredException | CertificateNotYetValidException e) {
            return false;
        }
        return true;
    }

    /**
     * Check whether the certificate's thumbprint matches the given one.
     * @return True if the thumbprints match. False otherwise.
     */
    private boolean thumbprintIsValid() throws NoSuchAlgorithmException, CertificateEncodingException {
        MessageDigest md = MessageDigest.getInstance("SHA-1");
        byte[] der = this.getCertificate().getEncoded();
        md.update(der);
        byte[] digest = md.digest();
        String digestHex = DatatypeConverter.printHexBinary(digest);
        return digestHex.toLowerCase().equals(this.getThumbprint().toLowerCase());
    }

    // Getters and setters.

    public void setThumbprint(String thumbprint) {
        this.thumbprint = thumbprint;
    }

    public String getThumbprint() {
        return this.thumbprint;
    }

    public X509Certificate getCertificate() {
        return certificate;
    }

    public void setCertificate(X509Certificate certificate) {
        this.certificate = certificate;
    }
}

Python-Beispiel

Die folgenden Flask- und Django Python-Codebeispiele implementieren ein Decorator-Element mit dem Namen authorize_certificate, das für eine Ansichtsfunktion verwendet werden kann, um den Zugriff nur für Aufrufer zu ermöglichen, die ein gültiges Clientzertifikat bereitstellen. Es erwartet ein PEM-formatiertes Zertifikat im X-ARR-ClientCert Header und verwendet das Python-Kryptografiepaket um das Zertifikat basierend auf seinem Fingerabdruck, dem allgemeinen Namen des Antragstellers, dem allgemeinen Namen des Herausgebers sowie dem Anfangs- und Ablaufdatum zu überprüfen. Wenn die Überprüfung fehlschlägt, stellt das Decorator-Element sicher, dass eine HTTP-Antwort mit dem Statuscode 403 (Verboten) an den Client zurückgegeben wird.

from functools import wraps
from datetime import datetime, timezone
from flask import abort, request
from cryptography import x509
from cryptography.x509.oid import NameOID
from cryptography.hazmat.primitives import hashes


def validate_cert(request):

    try:
        cert_value =  request.headers.get('X-ARR-ClientCert')
        if cert_value is None:
            return False
        
        cert_data = ''.join(['-----BEGIN CERTIFICATE-----\n', cert_value, '\n-----END CERTIFICATE-----\n',])
        cert = x509.load_pem_x509_certificate(cert_data.encode('utf-8'))
    
        fingerprint = cert.fingerprint(hashes.SHA1())
        if fingerprint != b'12345678901234567890':
            return False
        
        subject = cert.subject
        subject_cn = subject.get_attributes_for_oid(NameOID.COMMON_NAME)[0].value
        if subject_cn != "contoso.com":
            return False
        
        issuer = cert.issuer
        issuer_cn = issuer.get_attributes_for_oid(NameOID.COMMON_NAME)[0].value
        if issuer_cn != "contoso.com":
            return False
    
        current_time = datetime.now(timezone.utc)
    
        if current_time < cert.not_valid_before_utc:
            return False
        
        if current_time > cert.not_valid_after_utc:
            return False
        
        return True

    except Exception as e:
        # Handle any errors encountered during validation.
        print(f"Encountered the following error during certificate validation: {e}")
        return False
    
def authorize_certificate(f):
    @wraps(f)
    def decorated_function(*args, **kwargs):
        if not validate_cert(request):
            abort(403)
        return f(*args, **kwargs)
    return decorated_function

Der folgende Codeschnipsel zeigt, wie sie das Decorator-Element in einer Flask-Ansichtsfunktion verwenden.

@app.route('/hellocert')
@authorize_certificate
def hellocert():
   print('Request for hellocert page received')
   return render_template('index.html')