アプリに対してさまざまな種類の認証を有効にすることで、Azure App Service アプリへのアクセスを制限できます。 認証を設定する 1 つの方法は、トランスポート層セキュリティ (TLS) /Secure Sockets Layer (SSL) を使用してクライアント要求が送信されたときにクライアント証明書を要求し、証明書を検証することです。 このメカニズムは、 相互認証 または クライアント証明書認証と呼ばれます。 この記事では、クライアント証明書認証を使用するようにアプリを設定する方法について説明します。
注
アプリ コードでクライアント証明書を検証する必要があります。 App Service は、クライアント証明書をアプリに転送する以外に何も行いません。
HTTPS ではなく HTTP 経由でサイトにアクセスする場合、クライアント証明書は受け取りません。 アプリケーションでクライアント証明書が必要な場合は、HTTP 経由でアプリケーションへの要求を許可しないでください。
Web アプリの準備
カスタム TLS/SSL バインドを作成する場合、または App Service アプリのクライアント証明書を有効にする場合は、 App Service プラン が Basic、Standard、Premium、または Isolated レベルである必要があります。
Web アプリがサポートされている価格レベルにあることを確認するには、次の手順を実行します。
Web アプリに移動する
Azure portal の検索ボックスに「App Services」と入力し、検索結果で選択します。
[App Services] ページで、Web アプリを選択します。
これで、Web アプリの管理ページが表示されます。
価格レベルの確認
Web アプリの左側のメニューの [設定] で、[ スケールアップ ( App Service プラン)] を選択します。
Web アプリが F1 または D1 レベルに含まれていないことを確認します。 これらのレベルでは、カスタム TLS/SSL はサポートされていません。
スケール アップする必要がある場合は、次のセクションの手順に従います。 それ以外の場合は、[ スケールアップ ] ウィンドウを閉じて、次のセクションをスキップします。
App Service プランのスケール アップ
B1、B2、B3、運用カテゴリの他のレベルなど、非 Free レベルのいずれかを選びます。
完了したら [選択] を選択します。
スケール操作が完了すると、プランが更新されたことを示すメッセージが表示されます。
クライアント証明書を有効にする
アプリのクライアント証明書を有効にする場合は、クライアント証明書モードの選択を選択する必要があります。 このモードでは、アプリが受信クライアント証明書を処理する方法を定義します。 モードについては、次の表で説明します。
クライアント証明書モード | 説明 |
---|---|
必須 | すべての要求にはクライアント証明書が必要です。 |
省略可能 | 要求では、クライアント証明書を使用できます。 クライアントは、既定で証明書の入力を求められます。 たとえば、ブラウザー クライアントには、認証用の証明書を選択するためのプロンプトが表示されます。 |
オプションの対話ユーザー | 要求では、クライアント証明書を使用できます。 既定では、クライアントは証明書の入力を求められません。 たとえば、ブラウザー クライアントには、認証用の証明書を選択するプロンプトは表示されません。 |
Azure portal を使用してクライアント証明書を有効にするには:
- アプリ管理ページに移動します。
- 左側のメニューで、 構成>General 設定を選択します。
- [クライアント証明書モード] で、選択した項目を選択します。
- 保存 を選択します。
パスを認証を必要としないものとして除外する
アプリケーションの相互認証を有効にすると、アプリのルートにあるすべてのパスにアクセスするためのクライアント証明書が必要になります。 特定のパスでこの要件を削除するには、アプリケーション構成の一部として除外パスを定義します。
注
クライアント証明書の除外パスを使うと、アプリへの着信要求に対して TLS 再ネゴシエーションがトリガーされます。
アプリ管理ページの左側のメニューで、[設定]>[構成] を選択します。 [全般設定] タブを選択します。
[ 証明書の除外パス] の横にある鉛筆アイコンを選択します。
[ 新しいパス] を選択し、パスまたはパスの一覧を
,
または;
で区切って指定し、[ OK] を選択します。保存 を選択します。
次のスクリーンショットは、証明書の除外パスを設定する方法を示しています。 この例では、 /public
で始まるアプリのパスは、クライアント証明書を要求しません。 パスの一致では、大文字と小文字は区別されません。
クライアント証明書と TLS 再ネゴシエーション
クライアント証明書の一部の設定では、クライアント証明書を要求するかどうかを認識する前に要求を読み取るための TLS 再ネゴシエーションが App Service で必要となります。 次の設定の両方で、TLS 再ネゴシエーションがトリガーされます。
- オプションの 対話型ユーザー クライアント証明書モードの使用。
- クライアント証明書の除外パスの使用。
注
TLS 1.3 と HTTP 2.0 では、TLS 再ネゴシエーションはサポートされていません。 これらのプロトコルは、アプリが TLS 再ネゴシエーションを使用するクライアント証明書設定で構成されている場合は機能しません。
TLS 再ネゴシエーションを無効にし、TLS ハンドシェイク中にアプリでクライアント証明書をネゴシエートするには、アプリで次のアクションを実行する必要があります。
- クライアント証明書モードを [必須 ] または [省略可能] に設定します。
- すべてのクライアント証明書の除外パスを削除します。
TLS 再ネゴシエーションを使用して大きなファイルをアップロードする
TLS 再ネゴシエーションを使用するクライアント証明書の構成では、100 KB を超えるファイルで受信要求をサポートすることはできません。 この制限は、バッファー サイズの制限によって発生します。 このシナリオでは、100 KB を超える POST または PUT 要求は 403 エラーで失敗します。 この制限は構成可能ではなく、増やすことはできません。
100 KB の制限に対処するには、次の解決策を検討してください。
- 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) では、クライアント証明書は、 X-ARR-ClientCert
要求ヘッダーの Base64 でエンコードされた値を介して使用できます。
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 フォームのサンプル
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) 文字列を証明書オブジェクトに変換し、検証します。
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')