Freigeben über


Konfigurieren der gegenseitigen TLS-Authentifizierung für Azure App Service

Sie können den Zugriff auf Ihre Azure App Service-App einschränken, indem Sie verschiedene Authentifizierungstypen für sie aktivieren. Eine Möglichkeit dafür ist die Anforderung eines Clientzertifikats, wenn die Clientanforderung über TLS/SSL erfolgt, und die Überprüfung des Zertifikats. Dieser Mechanismus wird als „gegenseitige TLS-Authentifizierung“ oder „Clientzertifikatauthentifizierung“ bezeichnet. In diesem Artikel wird gezeigt, wie Sie Ihre App für die Verwendung der Clientzertifikatauthentifizierung einrichten.

Hinweis

Ihr App-Code ist für die Überprüfung des Clientzertifikats zuständig. App Service verwendet dieses Clientzertifikat nur für dessen Weiterleitung an Ihre App.

Wenn Sie über HTTP und nicht HTTPS auf Ihre Website zugreifen, erhalten Sie kein Clientzertifikat. Wenn Ihre Anwendung also Clientzertifikate erfordert, sollten Sie keine Anforderungen an Ihre Anwendung ü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, müssen Sie über einen App-Service-Plan im Tarif Basic, Standard, Premium oder Isolated verfügen. Um sicherzustellen, dass sich Ihre Web-App im unterstützten Tarif befindet, führen Sie die folgenden Schritte aus:

Wechseln zur Web-App

  1. Suchen Sie im Suchfeld des Azure-Portals nach App Services, und wählen Sie diese aus.

    Screenshot des Azure-Portals: Suchfeld mit Auswahl von „App Services“

  2. Wählen Sie auf der Seite App Services den Namen Ihrer Web-App aus.

    Screenshot: Seite „App Services“ im Azure-Portal mit einer Liste aller ausgeführten Web-Apps. Die erste App in der Liste ist hervorgehoben.

    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 im Abschnitt Einstellungen die Option Hochskalieren (App Service-Plan) aus.

    Screenshot: Web-App-Menü, Abschnitt „Einstellungen“ mit Auswahl von „Hochskalieren (App Service-Plan)“

  2. Stellen Sie sicher, dass für Ihre Web-App nicht der Tarif F1- oder D1 ausgewählt ist, der benutzerdefinierte TLS/SSL nicht unterstützt.

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

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. Klicken Sie auf Auswählen, wenn Sie fertig sind.

    Wenn die folgende Meldung angezeigt wird, ist der Skalierungsvorgang abgeschlossen.

    Screenshot mit Bestätigungsmeldung für die Hochskalierung

Aktivieren von Clientzertifikaten

So richten Sie Ihre App für das Erzwingen von Clientzertifikaten ein

  1. Wählen Sie auf der Verwaltungsseite Ihrer App im linken Navigationsbereich Konfiguration>Allgemeine Einstellungen aus.

  2. Wählen Sie den gewünschten Clientzertifikatmodus aus. Wählen Sie im oberen Bereich der Seite Speichern aus.

Clientzertifikatmodi Beschreibung
Erforderlich Für alle Anforderungen ist ein Clientzertifikat erforderlich.
Optional Anforderungen können, müssen aber kein Clientzertifikat verwenden. Clients werden standardmäßig zum Auswählen eines Zertifikats aufgefordert. Browserclients zeigen z. B. einen Prompt zum Auswählen eines Zertifikats für die Authentifizierung an.
Optionaler interaktiver Benutzer Anforderungen können, müssen aber kein Clientzertifikat verwenden. Clients werden standardmäßig nicht zum Auswählen eines Zertifikats aufgefordert. Browserclients zeigen z. B. keinen Prompt zum Auswählen eines Zertifikats für die Authentifizierung an.

Wenn Sie den Vorgang an der Azure-Befehlszeilenschnittstelle ausführen möchten, führen Sie den folgenden Befehl in der Cloud Shell aus:

az webapp update --set clientCertEnabled=true --name <app-name> --resource-group <group-name>

Ausschließen von Pfaden von der Anforderung der Authentifizierung

Wenn Sie die gegenseitige Authentifizierung für Ihre Anwendung aktivieren, erfordern 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.

  1. Wählen Sie auf der Verwaltungsseite Ihrer App im linken Navigationsbereich Konfiguration>Allgemeine Einstellungen aus.

  2. Wählen Sie neben der Option Zertifikatausschlusspfade das Symbol „Bearbeiten“ aus.

  3. Wählen Sie zunächst die Option 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 im oberen Bereich der Seite Speichern aus.

Im folgenden Screenshot wird für alle Pfade Ihrer App, die mit /public beginnen, kein Clientzertifikat angefordert. Bei der Pfadzuordnung wird die Groß-/Kleinschreibung nicht berücksichtigt.

Zertifikatausschlusspfade

Zugreifen auf das Clientzertifikat

In App Service erfolgt die TLS-Terminierung der Anforderung auf dem Front-End-Load Balancer. Wenn App Service die Anforderung an Ihren App-Code mit aktivierten Clientzertifikaten weiterleitet, wird ein X-ARR-ClientCert-Anforderungsheader mit dem Clientzertifikat eingefügt. App Service verwendet dieses Clientzertifikat nur für dessen Weiterleitung an Ihre App. Ihr App-Code ist für die Überprüfung des Clientzertifikats zuständig.

Für ASP.NET steht das Clientzertifikat über die Eigenschaft HttpRequest.ClientCertificate zur Verfügung.

Für andere Anwendungsstapel (Node.js, PHP usw.) steht das Clientzertifikat in Ihrer App über einen mit Base64 codierten Wert im Anforderungsheader X-ARR-ClientCert zur Verfügung.

ASP.NET Core-Beispiel

Für ASP.NET Core wird Middleware bereitgestellt, um weitergeleitete Zertifikate zu analysieren. Separate Middleware wird bereitgestellt, um die weitergeleiteten Protokollheader zu verwenden. Beides muss vorhanden sein, damit weitergeleitete Zertifikate akzeptiert werden können. Sie können benutzerdefinierte Zertifikatvalidierungslogik in den CertificateAuthentication-Optionen einsetzen.

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 forwared by the frontend load balancer
        services.Configure<ForwardedHeadersOptions>(options =>
        {
            options.ForwardedHeaders =
                ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
            // Only loopback proxies are allowed by default. Clear that restriction to enable this explicit configuration.
            options.KnownNetworks.Clear();
            options.KnownProxies.Clear();
        });       
        
        // Configure the application to client certificate forwarded the frontend load balancer
        services.AddCertificateForwarding(options => { options.CertificateHeader = "X-ARR-ClientCert"; });

        // Add certificate authentication so 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-WebForms

    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. Depending on your application logic and security requirements, 
            // you should modify this method
            //
            private bool IsValidClientCertificate()
            {
                // In this example we will only accept the certificate as a valid certificate if all the conditions below are met:
                // 1. The certificate isn't expired and is active for the current time on server.
                // 2. The subject name of the certificate has the common name nildevecc
                // 3. The issuer name of the certificate has the common name nildevecc and organization name Microsoft Corp
                // 4. The thumbprint of the certificate is 30757A2E831977D8BD9C8496E4C99AB26CB9622B
                //
                // This example doesn't test that this certificate is chained to a Trusted Root Authority (or revoked) on the server 
                // and it allows for self signed certificates
                //

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

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

                // 2. Check subject name of 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 issuer name of 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 thumprint of 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 Header X-ARR-ClientCert ab und verwendet node-forge zum Konvertieren der mit Base64 codierten PEM-Zeichenfolge in ein Zertifikatobjekt sowie dessen Überprüfung:

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.CERT
            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 mit dem im Konstruktor angegebenen übereinstimmt und 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 cannot 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 hasn't expired.
     * @return True if the certificate's thumbprint matches and hasn't expired. False otherwise.
     */
    public boolean certificateIsValid() throws NoSuchAlgorithmException, CertificateEncodingException {
        return certificateHasNotExpired() && thumbprintIsValid();
    }

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

    /**
     * Check the certificate's thumbprint matches the given one.
     * @return Returns 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 Zertifikat im PEM-Format im X-ARR-ClientCert-Header und verwendet das Python-Kryptografiepaket, um das Zertifikat basierend auf seinem Fingerabdruck, einem allgemeinen Antragstellernamen, einem allgemeinen Ausstellernamen und Anfangs- und Ablaufdaten 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')