Konfigurowanie wzajemnego uwierzytelniania TLS dla usługi Azure App Service
Dostęp do aplikacji usługi Azure App Service można ograniczyć, włączając dla niej różne typy uwierzytelniania. Jednym ze sposobów jest żądanie certyfikatu klienta, gdy żądanie klienta jest przesyłane za pośrednictwem protokołu TLS/SSL, i weryfikowanie tego certyfikatu. Ten mechanizm jest nazywany uwierzytelnianiem wzajemnym TLS lub uwierzytelnianiem certyfikatu klienta. W tym artykule wyjaśniono, jak skonfigurować aplikację w celu korzystania z uwierzytelniania certyfikatu klienta.
Uwaga
Kod aplikacji jest odpowiedzialny za weryfikowanie certyfikatu klienta. Usługa App Service nie wykonuje żadnych czynności z tym certyfikatem klienta innym niż przekazywanie go do aplikacji.
Jeśli uzyskujesz dostęp do witryny za pośrednictwem protokołu HTTP, a nie https, nie otrzymasz żadnego certyfikatu klienta. Jeśli więc aplikacja wymaga certyfikatów klienta, nie należy zezwalać na żądania do aplikacji za pośrednictwem protokołu HTTP.
Przygotowywanie aplikacji internetowej
Aby utworzyć niestandardowe powiązania TLS/SSL lub włączyć certyfikaty klienta dla aplikacji usługi App Service, plan usługi App Service musi znajdować się w warstwie Podstawowa, Standardowa, Premium lub Izolowana. Aby upewnić się, że aplikacja internetowa znajduje się w obsługiwanej warstwie cenowej, wykonaj następujące kroki:
Przejdź do aplikacji internetowej
W polu wyszukiwania witryny Azure Portal znajdź i wybierz pozycję App Services.
Na stronie App Services wybierz nazwę aplikacji internetowej.
Jesteś teraz na stronie zarządzania aplikacją internetową.
Sprawdzanie warstwy cenowej
W menu po lewej stronie aplikacji internetowej w sekcji Ustawienia wybierz pozycję Skaluj w górę (plan usługi App Service).
Upewnij się, że aplikacja internetowa nie znajduje się w warstwie F1 ani D1, która nie obsługuje niestandardowego protokołu TLS/SSL.
Jeśli musisz skalować w górę, wykonaj kroki opisane w następnej sekcji. W przeciwnym razie zamknij stronę Skalowanie w górę i pomiń sekcję Skalowanie w górę planu usługi App Service.
Skalowanie w górę planu usługi App Service
Wybierz dowolną warstwę inną niż bezpłatna, taką jak B1, B2, B3 lub dowolna inna warstwa w kategorii Produkcja.
Po zakończeniu wybierz pozycję Wybierz.
Po wyświetleniu następującego komunikatu operacja skalowania została ukończona.
Włączanie certyfikatów klienta
Aby skonfigurować aplikację w celu wymagania certyfikatów klienta:
Na lewym pasku nawigacyjnym strony zarządzania aplikacją wybierz pozycję Ustawienia ogólne konfiguracji>.
Wybierz pozycję Tryb certyfikatu klienta. W górnej części strony wybierz pozycję Zapisz.
Tryby certyfikatu klienta | opis |
---|---|
Wymagania | Wszystkie żądania wymagają certyfikatu klienta. |
Opcjonalnie | Żądania mogą lub nie mogą używać certyfikatu klienta. Klienci będą domyślnie monitowani o certyfikat. Na przykład klienci przeglądarki będą wyświetlać monit o wybranie certyfikatu na potrzeby uwierzytelniania. |
Opcjonalny interakcyjny użytkownik | Żądania mogą lub nie mogą używać certyfikatu klienta. Klienci nie będą domyślnie monitowani o certyfikat. Na przykład klienci przeglądarki nie będą wyświetlać monitu o wybranie certyfikatu na potrzeby uwierzytelniania. |
Aby wykonać to samo za pomocą interfejsu wiersza polecenia platformy Azure, uruchom następujące polecenie w usłudze Cloud Shell:
az webapp update --set clientCertEnabled=true --name <app-name> --resource-group <group-name>
Wykluczanie ścieżek z wymagania uwierzytelniania
Po włączeniu wzajemnego uwierzytelniania dla aplikacji wszystkie ścieżki w katalogu głównym aplikacji wymagają certyfikatu klienta w celu uzyskania dostępu. Aby usunąć to wymaganie dla niektórych ścieżek, zdefiniuj ścieżki wykluczeń w ramach konfiguracji aplikacji.
Na lewym pasku nawigacyjnym strony zarządzania aplikacją wybierz pozycję Ustawienia ogólne konfiguracji>.
Obok pozycji Ścieżki wykluczenia certyfikatu wybierz ikonę edycji.
Wybierz pozycję Nowa ścieżka, określ ścieżkę lub listę ścieżek rozdzielonych znakami
,
lub;
, a następnie wybierz przycisk OK.W górnej części strony wybierz pozycję Zapisz.
Na poniższym zrzucie ekranu dowolna ścieżka aplikacji rozpoczynająca się od /public
nie żąda certyfikatu klienta. Dopasowywanie ścieżki jest niewrażliwe na wielkość liter.
Uzyskiwanie dostępu do certyfikatu klienta
W usłudze App Service zakończenie żądania tls odbywa się w module równoważenia obciążenia frontonu. Gdy usługa App Service przekazuje żądanie do kodu aplikacji z włączonymi certyfikatami klienta, wprowadza X-ARR-ClientCert
nagłówek żądania z certyfikatem klienta. Usługa App Service nie wykonuje żadnych czynności z tym certyfikatem klienta innym niż przekazywanie go do aplikacji. Kod aplikacji jest odpowiedzialny za weryfikowanie certyfikatu klienta.
W przypadku ASP.NET certyfikat klienta jest dostępny za pośrednictwem właściwości HttpRequest.ClientCertificate .
W przypadku innych stosów aplikacji (Node.js, PHP itp.) certyfikat klienta jest dostępny w aplikacji za pośrednictwem wartości zakodowanej w formacie base64 w nagłówku X-ARR-ClientCert
żądania.
przykład ASP.NET Core
W przypadku ASP.NET Core oprogramowanie pośredniczące jest udostępniane do analizowania przekazanych certyfikatów. Oddzielne oprogramowanie pośredniczące jest udostępniane do używania nagłówków protokołu przekazywanego. Oba muszą być obecne, aby certyfikaty przekazywane zostały zaakceptowane. Niestandardową logikę weryfikacji certyfikatu można umieścić w opcjach 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?}");
});
}
}
przykład ASP.NET WebForms
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;
}
}
}
przykład Node.js
Poniższy przykładowy kod Node.js pobiera X-ARR-ClientCert
nagłówek i używa funkcji node-forge do konwertowania ciągu PEM zakodowanego w formacie base64 na obiekt certyfikatu i jego weryfikacji:
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);
}
}
}
}
Przykład języka Java
Następująca klasa Java koduje certyfikat z X-ARR-ClientCert
do X509Certificate
wystąpienia. certificateIsValid()
Sprawdza, czy odcisk palca certyfikatu jest zgodny z odciskiem palca podanym w konstruktorze i że certyfikat nie wygasł.
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;
}
}
Przykład języka Python
Poniższe przykłady kodu platformy Flask i Django w języku Python implementują dekorator o nazwie authorize_certificate
, który może być używany w funkcji widoku, aby zezwolić na dostęp tylko do osób wywołujących, które przedstawiają prawidłowy certyfikat klienta. Oczekuje certyfikatu PEM sformatowanego w nagłówku X-ARR-ClientCert
i używa pakietu kryptografii języka Python do weryfikacji certyfikatu na podstawie odcisku palca (odcisku palca), nazwy pospolitej podmiotu, nazwy pospolitej wystawcy oraz dat rozpoczęcia i wygaśnięcia. Jeśli walidacja nie powiedzie się, dekorator gwarantuje, że odpowiedź HTTP z kodem stanu 403 (Zabronione) zostanie zwrócona do klienta.
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
Poniższy fragment kodu pokazuje, jak używać dekoratora w funkcji widoku platformy Flask.
@app.route('/hellocert')
@authorize_certificate
def hellocert():
print('Request for hellocert page received')
return render_template('index.html')