Aracılığıyla paylaş


Azure App Service için TLS karşılıklı kimlik doğrulamayı yapılandırma

Azure App Service uygulamanız için farklı kimlik doğrulama türlerini etkinleştirerek bu uygulamaya erişimi kısıtlayabilirsiniz. Bunu gerçekleştirmenin bir yolu, TLS/SSL üzerinden gelen istemci isteğinde bir istemci sertifikası istemek ve sertifikayı doğrulamaktır. Bu mekanizma TLS karşılıklı kimlik doğrulama veya istemci sertifikası kimlik doğrulaması olarak adlandırılır. Bu makalede uygulamanızı istemci sertifikası kimlik doğrulaması kullanacak şekilde ayarlama işlemi gösterilir.

Not

Sitenize HTTPS üzerinden değil HTTP üzerinden erişirseniz, herhangi bir istemci sertifikası almazsınız. Bu nedenle, uygulamanız istemci sertifikaları gerektiriyorsa HTTP üzerinden uygulamanıza yönelik isteklere izin vermemelisiniz.

Web uygulamanızı hazırlama

App Service uygulamanız için özel TLS/SSL bağlamaları oluşturmak veya istemci sertifikalarını etkinleştirmek için App Service planınızın Temel, Standart, Premium veya Yalıtılmış katmanında olması gerekir. Web uygulamanızın desteklenen fiyatlandırma katmanında olduğundan emin olmak için şu adımları izleyin:

Web uygulamanıza gidin

  1. Azure portalı arama kutusunda Uygulama Hizmetleri'ni bulun ve seçin.

    Azure portalı, arama kutusu ve

  2. App Services sayfasında web uygulamanızın adını seçin.

    Listede ilk uygulamanın vurgulandığı, çalışan tüm web uygulamalarının listesini gösteren Azure portalındaki App Services sayfasının ekran görüntüsü.

    Artık web uygulamanızın yönetim sayfasındasınız.

Fiyatlandırma katmanını denetleme

  1. Web uygulamanızın sol menüsünde, Ayarlar bölümünün altında Ölçeği artırma (App Service planı) seçeneğini belirleyin.

    Web uygulaması menüsünün,

  2. Web uygulamanızın özel TLS/SSL'yi desteklemeyen F1 veya D1 katmanında olmadığından emin olun.

  3. Ölçeği artırmanız gerekirse sonraki bölümde verilen adımları izleyin. Aksi takdirde Ölçeği artır sayfasını kapatın ve App Service planınızın ölçeğini artırma bölümünü atlayın.

App Service planınızın ölçeğini artırma

  1. B1, B2, B3 gibi ücretsiz olmayan herhangi bir katmanı veya Üretim kategorisindeki başka bir katmanı seçin.

  2. İşiniz bittiğinde Seç'i seçin.

    Aşağıdaki ileti görüntülendiğinde ölçeklendirme işlemi tamamlanır.

    Ölçeği artırma işlemi için onay iletisini içeren ekran görüntüsü.

İstemci sertifikalarını etkinleştirme

Uygulamanızı istemci sertifikaları gerektirecek şekilde ayarlamak için:

  1. Uygulamanızın yönetim sayfasının sol gezinti bölmesinde Yapılandırma>Genel Ayarları'nı seçin.

  2. İstemci sertifikası modunu Gerekli olarak ayarlayın. Sayfanın üst kısmında Kaydet'i seçin.

Azure CLI ile aynı işlemi yapmak için Cloud Shell'de aşağıdaki komutu çalıştırın:

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

Yolları kimlik doğrulaması gerektirmeden dışlama

Uygulamanız için karşılıklı kimlik doğrulamayı etkinleştirdiğinizde, uygulamanızın kökü altındaki tüm yollar erişim için bir istemci sertifikası gerektirir. Belirli yollarda bu gereksinimi kaldırmak için, uygulama yapılandırmanızın bir parçası olarak dışlama yollarını tanımlayın.

  1. Uygulamanızın yönetim sayfasının sol gezinti bölmesinde Yapılandırma>Genel Ayarları'nı seçin.

  2. Sertifika dışlama yolları'nın yanında düzenle simgesini seçin.

  3. Yeni yol'a tıklayın, bir yol veya veya ;ile , ayrılmış yolların listesini belirtin ve Tamam'ı seçin.

  4. Sayfanın üst kısmında Kaydet'i seçin.

Aşağıdaki ekran görüntüsünde, uygulamanızın /public başlangıç yolu istemci sertifikası istemez. Yol eşleştirme büyük/küçük harfe duyarlı değildir.

Sertifika Dışlama Yolları

İstemci sertifikasına erişme

App Service'te isteğin TLS sonlandırması ön uç yük dengeleyicide gerçekleşir. App Service, istemci sertifikaları etkinleştirilmiş olarak isteği uygulama kodunuzla ilettiğinde, istemci sertifikasına bir X-ARR-ClientCert istek üst bilgisi ekler. App Service, bu istemci sertifikasıyla uygulamanıza iletmek dışında hiçbir şey yapmaz. uygulama kodunuz istemci sertifikasını doğrulamakla sorumludur.

ASP.NET için istemci sertifikası HttpRequest.ClientCertificate özelliği aracılığıyla kullanılabilir.

Diğer uygulama yığınları (Node.js, PHP vb.) için istemci sertifikası, uygulamanızda istek üst bilgisindeki X-ARR-ClientCert base64 kodlanmış değeri aracılığıyla kullanılabilir.

ASP.NET Core örneği

ASP.NET Core için, iletilen sertifikaları ayrıştırmak için ara yazılım sağlanır. İletilen protokol üst bilgilerini kullanmak için ayrı ara yazılım sağlanır. İletilen sertifikaların kabul edilmesi için her ikisi de mevcut olmalıdır. Özel sertifika doğrulama mantığını CertificateAuthentication seçeneklerine yerleştirebilirsiniz.

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

ASP.NET WebForms örneği

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

Node.js örneği

Aşağıdaki Node.js örnek kod üst bilgisini alır X-ARR-ClientCert ve base64 kodlu PEM dizesini bir sertifika nesnesine dönüştürmek ve doğrulamak için node-forge kullanır:

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 örneği

Aşağıdaki Java sınıfı, sertifikayı öğesinden X-ARR-ClientCert bir X509Certificate örneğe kodlar. certificateIsValid() sertifikanın parmak izinin oluşturucuda verilen parmak iziyle eşleşip eşleşmediğini ve sertifikanın süresinin dolmadığını doğrular.

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 örneği

Aşağıdaki Flask ve Django Python kod örnekleri, yalnızca geçerli bir istemci sertifikası sunan arayanlara erişim izni vermek için görünüm işlevinde kullanılabilen adlı authorize_certificate bir dekoratör uygular. Üst bilgide X-ARR-ClientCert PEM biçimli bir sertifika bekler ve parmak izi (parmak izi), konu ortak adı, veren ortak adı ve başlangıç ve son kullanma tarihleri temelinde sertifikayı doğrulamak için Python şifreleme paketini kullanır. Doğrulama başarısız olursa, dekoratör 403 (Yasak) durum koduna sahip bir HTTP yanıtının istemciye döndürülmesini sağlar.

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

Aşağıdaki kod parçacığı, Flask görünüm işlevinde dekoratörün nasıl kullanılacağını gösterir.

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