Konfigurace vzájemného ověřování TLS pro službu Azure App Service

Přístup k vaší aplikaci Azure App Service můžete omezit povolením různých typů ověřování. Jedním ze způsobů je vyžádat si klientský certifikát, pokud se požadavek klienta provádí přes TLS/SSL, a ověřit ho. Tento mechanismus se označuje jako vzájemné ověřování TLS nebo ověřování klientských certifikátů. V tomto článku se dozvíte, jak nastavit aplikaci tak, aby používala ověřování klientských certifikátů.

Poznámka:

Kód vaší aplikace zodpovídá za ověření klientského certifikátu. App Service s tímto klientským certifikátem nedělá nic jiného než jeho předávání do vaší aplikace.

Pokud k webu přistupujete přes PROTOKOL HTTP a ne HTTPS, nebudete dostávat žádný klientský certifikát. Takže pokud vaše aplikace vyžaduje klientské certifikáty, neměli byste povolit požadavky na vaši aplikaci přes protokol HTTP.

Příprava webové aplikace

Pokud chcete vytvořit vlastní vazby TLS/SSL nebo povolit klientské certifikáty pro aplikaci App Service, musí být váš plán služby App Service na úrovni Basic, Standard, Premium nebo Isolated . Pokud chcete zajistit, aby vaše webová aplikace byla v podporované cenové úrovni, postupujte takto:

Přechod na webovou aplikaci

  1. Ve vyhledávacím poli webu Azure Portal vyhledejte a vyberte App Services.

    Snímek obrazovky webu Azure Portal, vyhledávacího pole a vybrané služby App Services

  2. Na stránce App Services vyberte název vaší webové aplikace.

    Snímek obrazovky se stránkou App Services na webu Azure Portal zobrazující seznam všech spuštěných webových aplikací se zvýrazněnou první aplikací v seznamu

    Teď jste na stránce pro správu vaší webové aplikace.

Kontrola cenové úrovně

  1. V nabídce vlevo pro webovou aplikaci v části Nastavení vyberte Vertikálně navýšit kapacitu (plán služby App Service).

    Snímek obrazovky s nabídkou webové aplikace, oddílem Nastavení a vybranou možností Vertikální navýšení kapacity (plán služby App Service)

  2. Ujistěte se, že vaše webová aplikace není ve vrstvě F1 nebo D1 , která nepodporuje vlastní protokol TLS/SSL.

  3. Pokud potřebujete vertikálně navýšit kapacitu, postupujte podle kroků v další části. V opačném případě zavřete stránku Vertikální navýšení kapacity a přeskočte oddíl Vertikální navýšení kapacity plánu služby App Service.

Vertikální navýšení kapacity plánu služby App Service

  1. Vyberte libovolnou úroveň, která není bezplatná, například B1, B2, B3 nebo jakoukoli jinou úroveň v kategorii Production.

  2. Až budete hotovi, vyberte Vybrat.

    Jakmile se zobrazí následující zpráva, operace škálování se dokončila.

    Snímek obrazovky s potvrzovací zprávou pro operaci vertikálního navýšení kapacity

Povolení klientských certifikátů

Nastavení aplikace tak, aby vyžadovala klientské certifikáty:

  1. V levém navigačním panelu stránky pro správu vaší aplikace vyberte Obecné nastavení konfigurace>.

  2. Vyberte režim klientského certifikátu podle výběru. V horní části stránky vyberte Uložit.

Režimy klientských certifikátů Popis
Povinní účastníci Všechny požadavky vyžadují klientský certifikát.
Volitelné Požadavky mohou nebo nemusí používat klientský certifikát. Ve výchozím nastavení se klientům zobrazí výzva k zadání certifikátu. Například klienti prohlížeče zobrazí výzvu k výběru certifikátu pro ověření.
Volitelný interaktivní uživatel Požadavky mohou nebo nemusí používat klientský certifikát. Klienti nebudou ve výchozím nastavení vyzváni k zadání certifikátu. Například klienti prohlížeče nezobrazí výzvu k výběru certifikátu pro ověření.

Pokud chcete totéž provést s Azure CLI, spusťte v Cloud Shellu následující příkaz:

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

Vyloučení cest z vyžadování ověřování

Když povolíte vzájemné ověřování pro vaši aplikaci, všechny cesty pod kořenem aplikace vyžadují pro přístup klientský certifikát. Pokud chcete tento požadavek odebrat pro určité cesty, definujte cesty vyloučení jako součást konfigurace aplikace.

  1. V levém navigačním panelu stránky pro správu vaší aplikace vyberte Obecné nastavení konfigurace>.

  2. Vedle cest vyloučení certifikátu vyberte ikonu pro úpravy.

  3. Vyberte Možnost Nová cesta, zadejte cestu nebo seznam cest oddělených sadou , nebo ;a vyberte OK.

  4. V horní části stránky vyberte Uložit.

Na následujícím snímku obrazovky se žádná cesta pro vaši aplikaci, která začíná /public , nevyžaduje klientský certifikát. Porovnávání cest nerozlišuje malá a velká písmena.

Cesty vyloučení certifikátu

Přístup k klientskému certifikátu

Ve službě App Service dochází k ukončení protokolu TLS požadavku v nástroji pro vyrovnávání zatížení front-endu. Když App Service předá požadavek kódu vaší aplikace s povolenými klientskými certifikáty, vloží hlavičku X-ARR-ClientCert požadavku s klientským certifikátem. App Service s tímto klientským certifikátem nedělá nic jiného než jeho předávání do vaší aplikace. Kód vaší aplikace zodpovídá za ověření klientského certifikátu.

Pro ASP.NET je klientský certifikát dostupný prostřednictvím vlastnosti HttpRequest.ClientCertificate .

Pro ostatní zásobníky aplikací (Node.js, PHP atd.) je klientský certifikát dostupný ve vaší aplikaci prostřednictvím hodnoty zakódované v hlavičce požadavku s X-ARR-ClientCert kódováním Base64.

Ukázka ASP.NET Core

Pro ASP.NET Core je k dispozici middleware pro parsování předávaných certifikátů. Pro použití hlaviček předávaného protokolu je k dispozici samostatný middleware. Obě musí být k dispozici, aby se předávané certifikáty přijímaly. Vlastní logiku ověření certifikátu můžete umístit do možností 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?}");
        });
    }
}

Ukázka webovýchformulců ASP.NET

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

ukázka Node.js

Následující Node.js vzorový kód získá hlavičku X-ARR-ClientCert a pomocí forge uzlu převede řetězec PEM s kódováním base64 na objekt certifikátu a ověří ho:

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

Ukázka Javy

Následující třída Java kóduje certifikát z X-ARR-ClientCert instance X509Certificate . certificateIsValid() ověří, že kryptografický otisk certifikátu odpovídá kryptografickému otisku certifikátu zadanému v konstruktoru a že jeho platnost nevypršela.

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

Ukázka Pythonu

Následující ukázky kódu Flask a Django Python implementují dekorátor s názvem authorize_certificate , který lze použít ve funkci zobrazení, aby byl povolen přístup pouze volajícím, kteří prezentují platný klientský certifikát. Očekává v X-ARR-ClientCert hlavičce naformátovaný certifikát PEM a pomocí balíčku kryptografie Pythonu ověří certifikát na základě otisku prstu (kryptografického otisku), společného názvu subjektu, společného názvu vystavitele a data začátku a vypršení platnosti. Pokud ověření selže, dekorátor zajistí, aby se klientovi vrátila odpověď HTTP se stavovým kódem 403 (Zakázáno).

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

Následující fragment kódu ukazuje, jak používat dekorátor ve funkci zobrazení Flask.

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