A TLS kölcsönös hitelesítésének konfigurálása az Azure App Service számára
A Azure App Service alkalmazáshoz való hozzáférést a különböző típusú hitelesítés engedélyezésével korlátozhatja. Ennek egyik módja az ügyféltanúsítvány kérése, ha az ügyfélkérés TLS/SSL protokollon keresztül történik, és ellenőrzi a tanúsítványt. Ezt a mechanizmust TLS kölcsönös hitelesítésnek vagy ügyféltanúsítvány-hitelesítésnek nevezzük. Ez a cikk bemutatja, hogyan állíthatja be az alkalmazást ügyféltanúsítvány-hitelesítés használatára.
Megjegyzés
Ha HTTP-en keresztül éri el a webhelyet, és nem HTTPS-en keresztül, akkor nem kap ügyféltanúsítványt. Ha tehát az alkalmazás ügyféltanúsítványokat igényel, ne engedélyezze az alkalmazásnak HTTP-en keresztüli kéréseket.
A webalkalmazás előkészítése
Egyéni TLS-/SSL-kötések létrehozásához vagy az ügyféltanúsítványok App Service alkalmazáshoz való engedélyezéséhez a App Service csomagnakalapszintű, standard, prémium vagy izolált szinten kell lennie. Ha meg szeretné győződni arról, hogy a webalkalmazás a támogatott tarifacsomagban van, kövesse az alábbi lépéseket:
Ugrás a webalkalmazásra
A Azure Portal keresőmezőben keresse meg és válassza az App Services elemet.
Az App Services lapon válassza ki a webalkalmazás nevét.
Most már a webalkalmazás felügyeleti oldalán van.
A tarifacsomag ellenőrzése
A webalkalmazás bal oldali menüjében, a Beállítások szakaszban válassza a Vertikális felskálázás (App Service csomag) lehetőséget.
Győződjön meg arról, hogy a webalkalmazás nem az F1 vagy D1 szinten van, amely nem támogatja az egyéni TLS/SSL-t.
Ha vertikális felskálázásra van szüksége, kövesse az alábbi szakaszban található lépéseket. Ellenkező esetben zárja be a Vertikális felskálázás lapot, és hagyja ki a App Service csomag vertikális felskálázása szakaszt.
Az App Service-csomag vertikális felskálázása
Válasszon ki egy nem ingyenes szintet, például b1, B2, B3 vagy bármely más szintet az Éles kategóriában.
Ha elkészült, válassza a Kiválasztás lehetőséget.
Amikor megjelenik a következő üzenet, a skálázási művelet befejeződött.
Ügyféltanúsítványok engedélyezése
Az alkalmazás beállítása ügyféltanúsítványok megkövetelésére:
Az alkalmazás felügyeleti oldalának bal oldali navigációs sávján válassza azÁltalános beállításokkonfigurálása> lehetőséget.
Állítsa az Ügyféltanúsítvány üzemmódotKötelező értékre. Kattintson az oldal tetején lévő Mentés elemre.
Ha ugyanezt szeretné tenni az Azure CLI-vel, futtassa a következő parancsot a Cloud Shell:
az webapp update --set clientCertEnabled=true --name <app-name> --resource-group <group-name>
Elérési utak kizárása hitelesítés megkövetelése alól
Amikor engedélyezi a kölcsönös hitelesítést az alkalmazáshoz, az alkalmazás gyökerében található összes elérési úthoz ügyféltanúsítvány szükséges a hozzáféréshez. Bizonyos elérési utakra vonatkozó követelmény eltávolításához definiáljon kizárási útvonalakat az alkalmazáskonfiguráció részeként.
Az alkalmazás felügyeleti oldalának bal oldali navigációs sávján válassza azÁltalános beállításokkonfigurálása> lehetőséget.
A Tanúsítványkizárási útvonalak mellett kattintson a szerkesztés ikonra.
Kattintson az Új elérési út elemre, adjon meg egy elérési utat vagy a vagy
;
által,
elválasztott elérési utak listáját, majd kattintson az OK gombra.Kattintson az oldal tetején lévő Mentés elemre.
Az alábbi képernyőképen az alkalmazás bármely olyan elérési útja, amely a következővel /public
kezdődik, nem kér ügyféltanúsítványt. Az elérési út egyeztetése nem érzéketlen a kis- és nagybetűk között.
Ügyféltanúsítvány elérése
A App Service A kérés TLS-leállítása az előtérbeli terheléselosztónál történik. Amikor az ügyféltanúsítványokat engedélyezve továbbítja a kérést az alkalmazáskódnak, App Service egy kérelemfejlécet szúr be az ügyféltanúsítványbaX-ARR-ClientCert
. App Service ezzel az ügyféltanúsítvánnyal csak az alkalmazásnak továbbítja. Az alkalmazáskód felel az ügyféltanúsítvány érvényesítéséért.
A ASP.NET esetében az ügyféltanúsítvány a HttpRequest.ClientCertificate tulajdonságon keresztül érhető el.
Más alkalmazásvermek (Node.js, PHP stb.) esetén az ügyféltanúsítvány elérhető az alkalmazásban egy base64 kódolású értéken keresztül a X-ARR-ClientCert
kérés fejlécében.
ASP.NET 5+, ASP.NET Core 3.1 minta
A ASP.NET Core esetében a köztes szoftver biztosítja a továbbított tanúsítványok elemzését. A továbbított protokollfejlécek használatához külön köztes szoftver érhető el. A továbbított tanúsítványok elfogadásához mindkettőnek jelen kell lennie. Az egyéni tanúsítványérvényesítési logikát a CertificateAuthentication beállításai között helyezheti el.
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-minta
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 is not 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 does NOT 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 minta
A következő Node.js mintakód lekéri a X-ARR-ClientCert
fejlécet, és csomópont-kovácsolás használatával konvertálja a base64 kódolású PEM-sztringet tanúsítványobjektummá, és érvényesíti azt:
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-minta
A következő Java-osztály a tanúsítványt egy X509Certificate
példányba X-ARR-ClientCert
kódolja. certificateIsValid()
ellenőrzi, hogy a tanúsítvány ujjlenyomata megegyezik-e a konstruktorban megadott ujjlenyomattal, és hogy a tanúsítvány még nem járt le.
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 has not expired.
* @return True if the certificate's thumbprint matches and has not expired. False otherwise.
*/
public boolean certificateIsValid() throws NoSuchAlgorithmException, CertificateEncodingException {
return certificateHasNotExpired() && thumbprintIsValid();
}
/**
* Check certificate's timestamp.
* @return Returns true if the certificate has not 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;
}
}