Configuración de la autenticación mutua de TLS en Azure App Service
Puede restringir el acceso a una aplicación de Azure App Service habilitando diferentes tipos de autenticación. Una manera de hacerlo consiste en solicitar un certificado de cliente cuando la solicitud de cliente se realice a través de TLS/SSL y validar el certificado. Este mecanismo se denomina autenticación mutua de TLS 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 es responsable de validar el certificado de cliente. App Service no hace nada con este certificado de cliente que no sea reenviarlo a la aplicación.
Si accede a su sitio a través de HTTP y no de HTTPS, no recibirá ningún certificado de cliente. Por lo tanto, si la aplicación requiere certificados de cliente, no debe permitir solicitudes a la aplicación mediante HTTP.
Preparar la aplicación web
Para crear enlaces de TLS o SSL personalizados o habilitar certificados de cliente para la aplicación de App Service, el plan de App Service debe ser de nivel Básico, Estándar, Premium o Aislado. Para asegurarse de que la aplicación web se encuentra en el plan de tarifa compatible, siga estos pasos:
Vaya a la aplicación web.
En el cuadro de búsqueda de Azure Portal, busque y seleccione App Services.
En la página App Services, seleccione el nombre de la aplicación web.
Ahora está en la página de administración de la aplicación web.
Comprobar el plan de tarifa
En el menú de la izquierda de la aplicación web, en la sección Configuración, seleccione Escalar verticalmente (plan de App Service).
Asegúrese de que la aplicación web no está en el plan F1 o D1, que no admite TLS/SSL personalizado.
Si tiene que escalar verticalmente, siga los pasos de la sección siguiente. De lo contrario, cierre la página Escalar verticalmente y omita la sección Escalado vertical del plan de App Service.
Escalar verticalmente el plan de App Service
Seleccione cualquiera de los planes no gratuitos, como B1, B2, B3, o cualquier otro de la categoría Producción.
Cuando finalice, haga clic en Seleccionar.
Cuando aparezca el mensaje siguiente, se habrá completado la operación de escalado.
Habilitación de certificados de cliente
Para configurar la aplicación a fin de que requiera certificados de cliente:
En la navegación de la izquierda de la página de administración de la aplicación, seleccione Configuración>Configuración general.
Seleccione modo de certificado de cliente de elección. En la parte superior de la página, seleccione Guardar.
Modos de certificado de cliente | Descripción |
---|---|
Obligatorio | Todas las solicitudes requieren un certificado de cliente. |
Opcionales | Las solicitudes pueden o no usar un certificado de cliente. De forma predeterminada, se solicitará a los clientes un certificado. Por ejemplo, los clientes del explorador mostrarán un mensaje para seleccionar un certificado para la autenticación. |
Usuario interactivo opcional | Las solicitudes pueden o no usar un certificado de cliente. De forma predeterminada, no se solicitará un certificado a los clientes. Por ejemplo, los clientes del explorador no mostrarán un mensaje para seleccionar un certificado para la autenticación. |
Para hacer lo mismo con la CLI de Azure, ejecute el siguiente comando en Cloud Shell:
az webapp update --set clientCertEnabled=true --name <app-name> --resource-group <group-name>
Exclusión de rutas de acceso para que no requieran autenticación
Si habilita la autenticación mutua en su aplicación, todas las rutas de acceso situadas bajo la raíz de la aplicación necesitan un certificado de cliente para obtener 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.
En la navegación de la izquierda de la página de administración de la aplicación, seleccione Configuración>Configuración general.
Junto a rutas de exclusión de certificados, seleccione el icono de edición.
Seleccione Nueva ruta de acceso, especifique una ruta de acceso o una lista de rutas de acceso separadas por
,
o;
y seleccione Aceptar.En la parte superior de la página, seleccione Guardar.
En la captura de pantalla siguiente, cualquier ruta de acceso de la aplicación que comience con /public
no solicite un certificado de cliente. La coincidencia de rutas de acceso no distingue mayúsculas y minúsculas.
Acceso al certificado de cliente
En App Service, la terminación TLS de la solicitud tiene lugar en el equilibrador de carga de 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 es responsable de validar el certificado de cliente.
En ASP.NET, el certificado de cliente está disponible mediante la propiedad HttpRequest.ClientCertificate.
En otras pilas de aplicación (Node.js, PHP, etc.), el certificado de cliente está disponible en la aplicación mediante un valor codificado en base64 en el encabezado de solicitud X-ARR-ClientCert
.
Ejemplo de ASP.NET Core
Para ASP.NET Core, se proporciona middleware para analizar los certificados reenviados. Se proporciona middleware independiente 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 de 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?}");
});
}
}
Ejemplo de WebForms de 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;
}
}
}
Ejemplo de Node.js
El siguiente código de ejemplo de Node.js obtiene el encabezado X-ARR-ClientCert
y usa nodo-forge para convertir la cadena 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.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);
}
}
}
}
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 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;
}
}
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 encabezado X-ARR-ClientCert
y usa el paquete 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')