Compartir a través de


Configuración de la autenticación mutua TLS en Azure App Service

Para restringir el acceso a la aplicación de Azure App Service, habilite varios tipos de autenticación para la aplicación. Una manera de configurar la autenticación es solicitar un certificado de cliente cuando se envía la solicitud de cliente mediante la seguridad de la capa de transporte (TLS) / Capa de sockets seguros (SSL) y para validar el certificado. Este mecanismo se denomina autenticación mutua o autenticación de certificado de cliente. En este artículo se muestra cómo configurar la aplicación para usar la autenticación de certificado de cliente.

Nota:

El código de la aplicación debe validar el certificado de cliente. App Service no hace nada con el certificado de cliente que no sea reenviarlo a la aplicación.

Si accede al sitio a través de HTTP y no HTTPS, no recibirá ningún certificado de cliente. Si la aplicación requiere certificados de cliente, no debe permitir solicitudes a la aplicación a través de HTTP.

Preparar la aplicación web

Si desea crear enlaces TLS/SSL personalizados o habilitar certificados de cliente para la aplicación de App Service, el plan de App Service debe estar en los niveles Básico, Estándar, Premium o Aislado.

Para asegurarse de que la aplicación web está en un plan de tarifa compatible:

Vaya a la aplicación web.

  1. En el cuadro de búsqueda de Azure Portal , escriba App Services y selecciónelo en los resultados de búsqueda.

  2. En la página App Services , seleccione la aplicación web:

    Captura de pantalla de la página App Services en Azure Portal.

    Ahora está en la página de administración de la aplicación web.

Comprobar el plan de tarifa

  1. En el menú izquierdo de la aplicación web, en Configuración, seleccione Escalar verticalmente (plan de App Service).

  2. Asegúrese de que la aplicación web no está en el nivel F1 o D1. Estos niveles no admiten TLS/SSL personalizados.

  3. Si tiene que escalar verticalmente, siga los pasos de la sección siguiente. De lo contrario, cierre el panel Escalar verticalmente y omita la sección siguiente.

Escalar verticalmente el plan de App Service

  1. Seleccione cualquiera de los planes no gratuitos, como B1, B2, B3, o cualquier otro de la categoría Producción.

  2. Cuando haya terminado, elija Seleccionar.

    Una vez completada la operación de escalado, verá un mensaje que indica que el plan se actualizó.

Habilitación de certificados de cliente

Al habilitar certificados de cliente para la aplicación, debe seleccionar la opción del modo de certificado de cliente. El modo define cómo la aplicación controla los certificados de cliente entrantes. Los modos se describen en la tabla siguiente:

Modo de certificado de cliente Descripción
Obligatorio Todas las solicitudes requieren un certificado de cliente.
Opcionales Las solicitudes pueden usar un certificado de cliente. De forma predeterminada, se solicita a los clientes un certificado. Por ejemplo, los clientes del explorador muestran un mensaje para seleccionar un certificado para la autenticación.
Usuario interactivo opcional Las solicitudes pueden usar un certificado de cliente. A los clientes no se les solicita un certificado por defecto. Por ejemplo, los clientes del explorador no muestran un mensaje para seleccionar un certificado para la autenticación.

Para usar Azure Portal para habilitar los certificados de cliente:

  1. Vaya a la página de administración de aplicaciones.
  2. En el menú de la izquierda, seleccione Configuración>general.
  3. En Modo de certificado de cliente, seleccione su elección.
  4. Haga clic en Guardar.

Exclusión de rutas de acceso para que no requieran autenticación

Al habilitar la autenticación mutua para la aplicación, todas las rutas de acceso en la raíz de la aplicación requieren un certificado de cliente para el acceso. Para eliminar este requisito para determinadas rutas de acceso, defina las rutas de acceso de exclusión como parte de la configuración de la aplicación.

Nota:

El uso de cualquier ruta de exclusión de certificados de cliente desencadena la renegociación de TLS para las solicitudes entrantes a la aplicación.

  1. En el menú izquierdo de la página de administración de aplicaciones, seleccione Configuración.> Seleccione la pestaña Configuración general.

  2. Junto a rutas de exclusión de certificados, seleccione el icono del lápiz.

  3. Seleccione Nueva ruta de acceso, especifique una ruta de acceso o una lista de rutas separadas por , o ;y, a continuación, seleccione Aceptar.

  4. Haga clic en Guardar.

En la captura de pantalla siguiente se muestra cómo establecer una ruta de exclusión de certificado. En este ejemplo, cualquier ruta de acceso para la aplicación que comienza por /public no solicita un certificado de cliente. La coincidencia de rutas de acceso no es específica de mayúsculas y minúsculas.

Captura de pantalla que muestra cómo establecer una ruta de exclusión de certificado.

Renegociación de certificados de cliente y de TLS

Para algunos valores de certificado de cliente, App Service necesita la renegociación de TLS para leer una solicitud antes de saber si solicitar un certificado de cliente. Ambas opciones de configuración desencadenan la renegociación de TLS:

Nota:

TLS 1.3 y HTTP 2.0 no admiten la renegociación de TLS. Estos protocolos no funcionan si la aplicación está configurada con la configuración del certificado de cliente que usa la renegociación de TLS.

Para deshabilitar la renegociación de TLS y hacer que la aplicación negocie los certificados de cliente durante el intercambio de claves TLS, debe realizar las siguientes acciones en tu aplicación:

  • Establezca el modo de certificado de cliente en Obligatorio o Opcional.
  • Elimine todas las rutas de exclusión de certificados de cliente.

Carga de archivos grandes con renegociación TLS

Las configuraciones de certificados de cliente que usan la renegociación TLS no pueden admitir solicitudes entrantes con archivos superiores a 100 KB. Este límite se debe a las limitaciones del tamaño del búfer. En este escenario, cualquier solicitud POST o PUT que tenga más de 100 KB produce un error 403. Este límite no es configurable y no se puede aumentar.

Para abordar el límite de 100 KB, tenga en cuenta estas soluciones:

  • Deshabilitar la renegociación de TLS. Realice las siguientes acciones en las configuraciones de certificado de cliente de la aplicación:
    • Establezca el modo de certificado de cliente en Obligatorio o Opcional.
    • Elimine todas las rutas de exclusión de certificados de cliente.
  • Enviar una solicitud HEAD antes de la solicitud PUT/POST. La solicitud HEAD controla el certificado de cliente.
  • Agregar el encabezado Expect: 100-Continue a la solicitud. Este encabezado hace que el cliente espere hasta que el servidor responda con un 100 Continue antes de enviar el cuerpo de la solicitud y se omiten los búferes.

Acceso al certificado de cliente

En App Service, la terminación TLS de la solicitud se produce en el equilibrador de carga front-end. Cuando App Service reenvía la solicitud al código de la aplicación con certificados de cliente habilitados, inserta un encabezado de solicitud de X-ARR-ClientCert con el certificado de cliente. App Service no hace nada con este certificado de cliente que no sea reenviarlo a la aplicación. El código de la aplicación debe validar el certificado de cliente.

En ASP.NET, el certificado de cliente está disponible a través de la HttpRequest.ClientCertificate propiedad .

En otras pilas de aplicaciones (Node.js, PHP), el certificado de cliente está disponible a través de un valor codificado en Base64 en el encabezado de solicitud X-ARR-ClientCert.

Ejemplo de ASP.NET Core

Para ASP.NET Core, el middleware está disponible para analizar los certificados reenviados. El middleware independiente está disponible para usar los encabezados de protocolo reenviados. Ambos deben estar presentes para que se acepten los certificados reenviados. Puede colocar la lógica de validación de certificados personalizada en las opciones 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 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?}");
        });
    }
}

Ejemplo de ASP.NET Web Forms

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

Ejemplo de Node.js

El siguiente código de ejemplo Node.js obtiene el encabezado X-ARR-ClientCert y utiliza node-forge para convertir la cadena Privacy Enhanced Mail (PEM) codificada en Base64 en un objeto de certificado y validarlo.

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

Ejemplo de Java

La siguiente clase Java codifica el certificado de X-ARR-ClientCert a una instancia de X509Certificate. certificateIsValid() valida que la huella digital del certificado coincide con la especificada en el constructor y que el certificado no ha expirado.

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

Ejemplo de Python

Los siguientes ejemplos de código de Flask y Django Python implementan un decorador denominado authorize_certificate que se puede usar en una función de vista para permitir el acceso solo a los autores de llamadas que presentan un certificado de cliente válido. Espera un certificado con formato PEM en el X-ARR-ClientCert encabezado y usa el paquete de criptografía de Python para validar el certificado en función de su huella digital (huella digital), el nombre común del firmante, el nombre común del emisor y las fechas de inicio y expiración. Si se produce un error en la validación, el decorador garantiza que se devuelve una respuesta HTTP con el código de estado 403 (prohibido) al cliente.

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

El siguiente fragmento de código muestra cómo usar el decorador en una función de vista de Flask.

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