Delen via


Wederzijdse TLS-verificatie voor Azure App Service configureren

U kunt de toegang tot uw Azure App Service-app beperken door er verschillende typen verificatie voor in te schakelen. Een manier om dit te doen, is door een clientcertificaat aan te vragen wanneer de clientaanvraag TLS/SSL gebruikt en het certificaat te valideren. Dit mechanisme wordt wederzijdse TLS-verificatie of clientcertificaatverificatie genoemd. In dit artikel wordt beschreven hoe u uw app instelt voor het gebruik van clientcertificaatverificatie.

Notitie

Uw app-code is verantwoordelijk voor het valideren van het clientcertificaat. App Service doet niets met dit clientcertificaat anders dan het doorsturen naar uw app.

Als u toegang tot uw site hebt via HTTP en niet HTTPS, ontvangt u geen clientcertificaat. Als voor uw toepassing clientcertificaten zijn vereist, moet u dus geen aanvragen voor uw toepassing via HTTP toestaan.

Uw web-app voorbereiden

Als u aangepaste TLS-/SSL-bindingen wilt maken of clientcertificaten wilt inschakelen voor uw App Service-app, moet uw App Service-plan zich bevinden in de laag Basic, Standard, Premium of Isolated. Volg deze stappen om ervoor te zorgen dat uw web-app zich in de ondersteunde prijscategorie bevindt:

Ga naar uw web-app

  1. Zoek en selecteer App Services in het zoekvak van Azure Portal.

    Schermopname van Azure Portal, zoekvak en App Services geselecteerd.

  2. Selecteer op de pagina App Services de naam van uw web-app.

    Schermopname van de App Services-pagina in Azure Portal met een lijst van alle actieve web-apps, waarbij de eerste app in de lijst is gemarkeerd.

    U bent nu op de beheerpagina van uw web-app.

Controleer de prijscategorie

  1. Selecteer omhoog schalen (App Service-plan) in het linkermenu voor uw web-app in de sectie Instellingen.

    Schermopname van het menu 'Instellingen' en 'Omhoog schalen (App Service-plan)' geselecteerd.

  2. Zorg ervoor dat uw web-app zich niet in de F1 - of D1-laag bevindt, die geen ondersteuning biedt voor aangepaste TLS/SSL.

  3. Als u omhoog moet schalen, volgt u de stappen in het volgende gedeelte. Sluit anders de pagina Omhoog schalen en sla de sectie Uw App Service-plan omhoog schalen over.

Uw App Service-plan omhoog schalen

  1. Selecteer een niet-gratis laag, zoals B1, B2, B3 of een andere laag in de categorie Productie.

  2. Wanneer u klaar bent, selecteert u Selecteren.

    Wanneer het volgende bericht wordt weergegeven, is de schaalbewerking voltooid.

    Schermopname met bevestigingsbericht voor omhoog schalen.

Clientcertificaten inschakelen

Ga als volgende te werk om uw app in te stellen om clientcertificaten te vereisen:

  1. Selecteer in de linkernavigatiebalk van de beheerpagina van uw app de optie Algemene instellingen voor configuratie>.

  2. Selecteer de gewenste clientcertificaatmodus . Selecteer Opslaan boven aan de pagina.

Clientcertificaatmodi Beschrijving
Vereist Voor alle aanvragen is een clientcertificaat vereist.
Optioneel Aanvragen kunnen al dan niet gebruikmaken van een clientcertificaat. Clients worden standaard om een certificaat gevraagd. Browserclients geven bijvoorbeeld een prompt weer om een certificaat voor verificatie te selecteren.
Optionele interactieve gebruiker Aanvragen kunnen al dan niet gebruikmaken van een clientcertificaat. Clients worden niet standaard om een certificaat gevraagd. Browserclients geven bijvoorbeeld geen prompt weer om een certificaat voor verificatie te selecteren.

Als u hetzelfde wilt doen met Azure CLI, voert u de volgende opdracht uit in de Cloud Shell:

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

Paden uitsluiten van verificatie vereisen

Wanneer u wederzijdse verificatie voor uw toepassing inschakelt, hebben alle paden onder de hoofdmap van uw app een clientcertificaat nodig voor toegang. Als u deze vereiste voor bepaalde paden wilt verwijderen, definieert u uitsluitingspaden als onderdeel van uw toepassingsconfiguratie.

  1. Selecteer in de linkernavigatiebalk van de beheerpagina van uw app de optie Algemene instellingen voor configuratie>.

  2. Selecteer naast uitsluitingspaden voor certificaten het bewerkingspictogram.

  3. Selecteer Nieuw pad, geef een pad of een lijst met paden op, gescheiden door , of ;, en selecteer OK.

  4. Selecteer Opslaan boven aan de pagina.

In de volgende schermopname wordt een pad voor uw app waarmee wordt gestart /public , geen clientcertificaat aangevraagd. Padkoppeling is niet hoofdlettergevoelig.

Uitsluitingspaden voor certificaten

Toegang tot clientcertificaat

In App Service vindt TLS-beëindiging van de aanvraag plaats bij de front-end load balancer. Wanneer App Service de aanvraag doorstuurt naar uw app-code waarvoor clientcertificaten zijn ingeschakeld, wordt er een X-ARR-ClientCert aanvraagheader met het clientcertificaat geïnjecteerd. App Service doet niets met dit clientcertificaat anders dan het doorsturen naar uw app. Uw app-code is verantwoordelijk voor het valideren van het clientcertificaat.

Voor ASP.NET is het clientcertificaat beschikbaar via de eigenschap HttpRequest.ClientCertificate .

Voor andere toepassingsstacks (Node.js, PHP, enzovoort) is het clientcertificaat beschikbaar in uw app via een base64-gecodeerde waarde in de X-ARR-ClientCert aanvraagheader.

ASP.NET Core-voorbeeld

Voor ASP.NET Core wordt middleware geleverd om doorgestuurde certificaten te parseren. Afzonderlijke middleware wordt geleverd voor het gebruik van de doorgestuurde protocolheaders. Beide moeten aanwezig zijn om doorgestuurde certificaten te kunnen accepteren. U kunt aangepaste certificaatvalidatielogica plaatsen in de opties voor CertificateAuthentication.

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?}");
        });
    }
}

voorbeeld van 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;
            }
        }
    }

voorbeeld van Node.js

De volgende Node.js voorbeeldcode haalt de X-ARR-ClientCert header op en gebruikt node-forge om de met base64 gecodeerde PEM-tekenreeks te converteren naar een certificaatobject en te valideren:

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-voorbeeld

De volgende Java-klasse codeert het certificaat van X-ARR-ClientCert naar een X509Certificate exemplaar. certificateIsValid() valideert of de vingerafdruk van het certificaat overeenkomt met de vingerafdruk die is opgegeven in de constructor en dat certificaat niet is verlopen.

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-voorbeeld

In de volgende Flask- en Django Python-codevoorbeelden wordt een decorator geïmplementeerd authorize_certificate die kan worden gebruikt voor een weergavefunctie om alleen toegang te verlenen tot bellers die een geldig clientcertificaat presenteren. Er wordt een met PEM opgemaakt certificaat in de X-ARR-ClientCert header verwacht en het Python-cryptografiepakket wordt gebruikt om het certificaat te valideren op basis van de vingerafdruk (vingerafdruk), de algemene naam van het onderwerp, de algemene naam van de uitgever en de begin- en vervaldatum. Als de validatie mislukt, zorgt de decorator ervoor dat een HTTP-antwoord met statuscode 403 (Verboden) wordt geretourneerd naar de client.

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

In het volgende codefragment ziet u hoe u de decorator gebruikt in een Flask-weergavefunctie.

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