앱에 대해 다양한 유형의 인증을 사용하도록 설정하여 Azure App Service 앱에 대한 액세스를 제한할 수 있습니다. 인증을 설정하는 한 가지 방법은 TLS(전송 계층 보안) /SSL(Secure Sockets Layer)을 사용하여 클라이언트 요청을 보낼 때 클라이언트 인증서를 요청하고 인증서의 유효성을 검사하는 것입니다. 이 메커니즘을 상호 인증 또는 클라이언트 인증서 인증이라고 합니다. 이 문서에는 클라이언트 인증서 인증을 사용하도록 앱을 설정하는 방법이 나와 있습니다.
참고 항목
앱 코드는 클라이언트 인증서의 유효성을 검사해야 합니다. App Service는 클라이언트 인증서를 앱에 전달하는 것 외에는 아무 작업도 수행하지 않습니다.
HTTPS가 아닌 HTTP를 통해 사이트에 액세스하는 경우 클라이언트 인증서를 받지 못합니다. 애플리케이션에 클라이언트 인증서가 필요한 경우 HTTP를 통해 애플리케이션에 대한 요청을 허용해서는 안 됩니다.
웹앱 준비
사용자 지정 TLS/SSL 바인딩을 만들거나 App Service 앱에 대한 클라이언트 인증서를 사용하도록 설정하려면 App Service 계획이 기본, 표준, 프리미엄 또는 격리 계층에 있어야 합니다.
웹앱이 지원되는 가격 책정 계층에 있는지 확인하려면 다음을 수행합니다.
웹앱으로 이동
Azure Portal 검색 상자에 App Services를 입력한 다음 검색 결과에서 선택합니다.
App Services 페이지에서 웹앱을 선택합니다.
이제 웹앱의 관리 페이지에 있습니다.
가격 책정 계층 확인
웹앱에 대한 왼쪽 메뉴의 설정에서 강화(App Service 계획)를 선택합니다.
웹앱이 F1 또는 D1 계층에 있지 않은지 확인합니다. 이러한 계층은 사용자 지정 TLS/SSL을 지원하지 않습니다.
강화해야 하는 경우 다음 섹션의 단계를 수행합니다. 그렇지 않으면 확대 창을 닫고 다음 섹션을 건너뛰세요.
App Service 계획 강화
B1, B2, B3 또는 프로덕션 범주의 다른 계층과 같은 유료 계층을 선택합니다.
완료되면 선택을 선택합니다.
크기 조정 작업이 완료되면 계획이 업데이트되었다는 메시지가 표시됩니다.
클라이언트 인증서 사용하도록 설정
앱에 클라이언트 인증서를 사용하도록 설정하면 선택한 클라이언트 인증서 모드를 선택해야 합니다. 모드는 앱이 들어오는 클라이언트 인증서를 처리하는 방법을 정의합니다. 모드는 다음 표에 설명되어 있습니다.
클라이언트 인증서 모드 | 설명 |
---|---|
필수 | 모든 요청에는 클라이언트 인증서가 필요합니다. |
선택 사항 | 요청은 클라이언트 인증서를 사용할 수 있습니다. 클라이언트는 기본적으로 인증서를 입력하라는 메시지가 표시됩니다. 예를 들어 브라우저 클라이언트는 인증을 위해 인증서를 선택하라는 프롬프트를 표시합니다. |
선택적 대화형 사용자 | 요청은 클라이언트 인증서를 사용할 수 있습니다. 클라이언트는 기본적으로 인증서를 입력하라는 메시지가 표시되지 않습니다. 예를 들어 브라우저 클라이언트는 인증을 위해 인증서를 선택하라는 프롬프트를 표시하지 않습니다. |
Azure Portal을 사용하여 클라이언트 인증서를 사용하도록 설정하려면 다음을 수행합니다.
- 앱 관리 페이지로 이동합니다.
- 왼쪽 메뉴에서 구성>일반 설정을 선택합니다.
- 클라이언트 인증서 모드의 경우 원하는 항목을 선택합니다.
- 저장을 선택합니다.
인증 요구에서 경로 제외
애플리케이션에 대해 상호 인증을 사용하도록 설정하면 앱 루트 아래의 모든 경로에 액세스하기 위해 클라이언트 인증서가 필요합니다. 특정 경로에 대한 해당 요구 사항을 제거하려면 애플리케이션 구성의 일부로 제외 경로를 정의하세요.
참고 항목
클라이언트 인증서 제외 경로를 사용하면 앱에 들어오는 요청에 대한 TLS 재협상이 트리거됩니다.
앱 관리 페이지의 왼쪽 메뉴에서 설정>구성을 선택합니다. 일반 설정 탭을 선택합니다.
인증서 제외 경로 옆에 있는 연필 아이콘을 선택합니다.
새 경로를 선택하고, 경로나 경로 목록을
,
또는;
로 구분하여 지정한 후 확인을 선택합니다.저장을 선택합니다.
다음 스크린샷은 인증서 제외 경로를 설정하는 방법을 보여줍니다. 이 예제에서 시작하는 /public
앱의 경로는 클라이언트 인증서를 요청하지 않습니다. 경로 일치는 대/소문자를 구분하지 않습니다.
클라이언트 인증서 및 TLS 재협상
일부 클라이언트 인증서 설정의 경우 App Service는 클라이언트 인증서를 묻는 메시지를 표시할지 여부를 알기 전에 요청을 읽으려면 TLS 재협상이 필요합니다. 다음 설정은 모두 TLS 재협상을 트리거합니다.
- 선택적 대화형 사용자 클라이언트 인증서 모드 사용
- 클라이언트 인증서 제외 경로 사용
참고 항목
TLS 1.3 및 HTTP 2.0은 TLS 재협상을 지원하지 않습니다. 앱이 TLS 재협상을 사용하는 클라이언트 인증서 설정으로 구성된 경우에는 이러한 프로토콜이 작동하지 않습니다.
TLS 재협상을 사용하지 않도록 설정하고 TLS 핸드셰이크 중에 앱이 클라이언트 인증서를 협상하도록 하려면 앱에서 다음 작업을 수행해야 합니다.
- 클라이언트 인증서 모드를 필수 또는 선택 사항으로 설정합니다.
- 모든 클라이언트 인증서 제외 경로를 제거합니다.
TLS 재협상을 사용하여 대용량 파일 업로드
TLS 재협상을 사용하는 클라이언트 인증서 구성은 100KB보다 큰 파일로 들어오는 요청을 지원할 수 없습니다. 이 제한은 버퍼 크기 제한으로 인해 발생합니다. 이 시나리오에서는 100KB를 초과하는 POST 또는 PUT 요청이 403 오류로 실패합니다. 이 제한은 구성할 수 없으며 늘릴 수 없습니다.
100KB 제한을 해결하려면 다음 솔루션을 고려합니다.
- TLS 재협상을 사용하지 않도록 설정합니다. 앱의 클라이언트 인증서 구성에서 다음 작업을 수행합니다.
- 클라이언트 인증서 모드를 필수 또는 선택 사항으로 설정합니다.
- 모든 클라이언트 인증서 제외 경로를 제거합니다.
- PUT/POST 요청 전에 HEAD 요청을 보냅니다. HEAD 요청은 클라이언트 인증서를 처리합니다.
- 요청에 헤더
Expect: 100-Continue
를 추가합니다. 이 헤더를 사용하면 서버가 요청 본문을 보내기 전에 응답100 Continue
할 때까지 클라이언트가 대기하고 버퍼가 무시됩니다.
클라이언트 인증서 액세스
App Service에서 요청의 TLS 종료는 프런트 엔드 부하 분산 장치에서 발생합니다. App Service가 클라이언트 인증서를 사용하도록 설정된 상태로 앱 코드에 요청을 전달하면 클라이언트 인증서와 X-ARR-ClientCert
함께 요청 헤더가 삽입됩니다. App Service는 이 클라이언트 인증서를 앱에 전달하는 것 외에는 아무 작업도 수행하지 않습니다. 앱 코드는 클라이언트 인증서의 유효성을 검사해야 합니다.
ASP.NET 클라이언트 인증서는 속성을 통해 HttpRequest.ClientCertificate
사용할 수 있습니다.
다른 애플리케이션 스택(Node.js, PHP)에서 클라이언트 인증서는 요청 헤더의 Base64로 인코딩된 값을 X-ARR-ClientCert
통해 사용할 수 있습니다.
ASP.NET Core 샘플
ASP.NET Core의 경우 전달된 인증서를 구문 분석하는 데 미들웨어를 사용할 수 있습니다. 전달된 프로토콜 헤더를 사용하는 데 별도의 미들웨어를 사용할 수 있습니다. 전달된 인증서를 수락하려면 둘 다 있어야 합니다. 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?}");
});
}
}
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;
}
}
}
Node.js 샘플
다음 Node.js 샘플 코드는 헤더를 X-ARR-ClientCert
가져오고 node-forge 를 사용하여 Base64로 인코딩된 PEM(Privacy Enhanced Mail) 문자열을 인증서 개체로 변환하고 유효성을 검사합니다.
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);
}
}
}
}
Java 샘플
다음 Java 클래스는 인증서를 X-ARR-ClientCert
에서 X509Certificate
인스턴스로 인코딩합니다.
certificateIsValid()
인증서의 지문이 생성자에 지정된 지문과 일치하고 인증서가 만료되지 않았는지 확인합니다.
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;
}
}
Python 샘플
다음 Flask 및 Django Python 코드 샘플은 유효한 클라이언트 인증서를 제공하는 호출자에 대한 액세스를 허용하기 위해 뷰 함수에서 사용할 수 있는 데코레이터를 authorize_certificate
구현합니다. 헤더에 X-ARR-ClientCert
PEM 형식의 인증서가 필요한데 Python 암호화 패키지를 사용하여 지문(지문), 주체 일반 이름, 발급자 일반 이름, 시작 및 만료 날짜를 기반으로 인증서의 유효성을 검사합니다. 유효성 검사에 실패하면 데코레이터는 상태 코드 403(사용할 수 없음)이 있는 HTTP 응답이 클라이언트에 반환되도록 합니다.
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
다음 코드 조각은 Flask 뷰 함수에서 데코레이터를 사용하는 방법을 보여 줍니다.
@app.route('/hellocert')
@authorize_certificate
def hellocert():
print('Request for hellocert page received')
return render_template('index.html')