iPad をキオスクとして使う現場では、利用者のログインを排しつつも社外公開 API へ安全に接続する必要があります。本記事は .NET MAUI 製アプリと ASP.NET Core API を前提に、ユーザー認証を持たない代わりに「端末とアプリを厳密に信頼」するアーキテクチャを、設計・実装・運用の三層で具体化します。mTLS・MDM・API ゲートウェイの三位一体で“正規端末以外は到達不可”を実現します。
背景と前提条件の整理
状況
- iPad をキオスクモードで運用(現場で常時稼働、誰でも操作可能)。
- .NET MAUI で作成したクライアントから、インターネット公開された ASP.NET Core API を利用。
制約
- ユーザー認証(パスワード/OTP/ID 連携)は一切設けない。
- 端末は社内配布の iPad のみ。MDM による管理が可能。
課題
- 利用者を識別できない前提で、API を不正利用・不正スクリプトから守る。
- 漏洩時の影響最小化(証明書失効・端末紛失・改ざん検知)。
結論(先に要点)
- ユーザー認証ではなく端末認証へ転換:MDM 下の iPad + 指定アプリのみをトラストの単位にする。
- mTLS(相互 TLS)を必須:クライアント証明書未提示の通信は TCP レベルで拒否。
- API ゲートウェイ/WAFで多層防御:レート制限・Bot/異常検出・証明書失効チェック。
- キオスク固定:Single App Mode を MDM で強制、Guided Access は避ける。
- 証明書ライフサイクルを運用へ組み込み:短寿命+自動更新+即時失効が前提。
- アプリ改ざん・MITM対策:証明書ピンニング+脱獄検知+整合性チェック。
全体アーキテクチャ
構成要素と責務を明確にします。
| コンポーネント | 主な責務 | セキュリティ要点 |
|---|---|---|
| iPad(Single App Mode) | .NET MAUI アプリを専用起動し常時稼働 | MDM 制御、証明書配布、脱獄検知、OS/アプリ自動更新 |
| .NET MAUI アプリ | API 呼び出し・オフラインバッファ | mTLS クライアント証明書提示、サーバー証明書ピンニング |
| API ゲートウェイ/WAF | mTLS 終端・ルーティング・防御 | レート制限、Bot/ASN 制御、失効チェック、ヘッダ正規化 |
| ASP.NET Core API | 業務ロジック・データ I/O | クライアント証明書検証、スコープ制御、監査ログ |
| PKI / CA | クライアント証明書発行・失効・ローテーション | 短寿命+自動更新、CRL/OCSP、鍵の非エクスポート |
(概念フロー)
MAUI App ──mTLS──> API Gateway/WAF ──TLS──> ASP.NET Core API ──> DB
▲ ▲
│ └─ CRL/OCSP, レート制限, IP/ASN 制御, Bot ブロック
└─ MDM: 証明書配布/失効, Single App Mode, 設定プロファイル
キオスク固定:Single App Mode と Guided Access の違い
| モード | 管理方法 | 特徴 | 運用負荷 | リスク | 推奨度 |
|---|---|---|---|---|---|
| Single App Mode | MDM で強制 | 自動起動・終了不可、設定変更を抑止、証明書/プロファイル配布自動化 | 低(MDM ポリシーで一元管理) | 脱獄・物理改変時のみ脆弱 | ★★★ |
| Guided Access | 手動設定(端末操作) | 現場で一時固定は可能だが解除が容易、設定逸脱の検知が困難 | 高(現場依存、監査困難) | 人為的ミス・設定抜けが発生しやすい | ★☆☆ |
セキュリティを最優先するなら Single App Mode 一択です。
端末認証を中核に据える:MDM × mTLS
なぜユーザー認証をやめて端末認証なのか
- キオスクでは「誰が触るか」を識別できないため、「何が接続しているか」=管理下端末かどうかを強く担保する方が合理的。
- 端末証明書は自動提示され、パスワード漏洩やフィッシングのリスクを本質的に排除できる。
証明書設計の最小要件
| 項目 | 推奨値/方針 | 理由 |
|---|---|---|
| 鍵種別 | ECDSA P-256 以上(または RSA 3072) | パフォーマンスと強度のバランス |
| 有効期限 | 180~365 日(短寿命) | 漏洩時の影響を短縮、ローテーションを前提化 |
| SAN | デバイス識別子(シリアル等)、所属 OU | サーバー側でポリシー制御しやすい |
| 鍵保護 | Keychain(可能なら Secure Enclave)に非エクスポートで格納 | 私有鍵持ち出し防止 |
| 発行方式 | MDM 経由(SCEP/PKCS#12 配布) | 人手を介さないスケール運用 |
通信経路の強化:mTLS を標準装備
mTLS は「正規端末以外は TLS ハンドシェイクで拒否」できるため、API レイヤー前の段階で大半の攻撃面を消せます。
- ゲートウェイで mTLS を終端:クライアント証明書の検証(チェーン・失効・ポリシー)を実施し、問題なければバックエンドへ転送。
- バックエンドでも二重チェック:ゲートウェイから
X-Client-Certなどの安全なヘッダで証明書を転送する場合、該当ヘッダの偽装を確実に遮断(ゲートウェイ内でのみ付与、外部からの同名ヘッダを強制的に除去)。 - サーバー証明書ピンニング:クライアントは信頼する公開鍵フィンガープリントを内蔵し、MITM を防止。
.NET MAUI 側の実装ポイント
mTLS 用 HttpClient の設定
var handler = new HttpClientHandler
{
ClientCertificates = { /* Keychain から取得した X509Certificate2 */ },
SslProtocols = System.Security.Authentication.SslProtocols.Tls12 |
System.Security.Authentication.SslProtocols.Tls13,
ServerCertificateCustomValidationCallback = (req, cert, chain, errors) =>
{
// 1) 通常の証明書検証
var ok = errors == System.Net.Security.SslPolicyErrors.None;
// 2) ピンニング(SPKI の SHA-256)
ok &= IsPinned(cert);
return ok;
}
};
var http = new HttpClient(handler)
{
Timeout = TimeSpan.FromSeconds(30)
};
// 例:再試行やバックオフは Polly 等で実装
Keychain からクライアント証明書を取得する(iOS)
MDM で配布したアイデンティティ(証明書+秘密鍵)を Keychain から参照し、X509Certificate2 として HttpClientHandler.ClientCertificates に追加します。
#if IOS
using Security;
using System.Security.Cryptography.X509Certificates;
// ラベルやアクセスグループは MDM 配布時の設定に合わせる
var query = new SecRecord(SecKind.Identity)
{
Label = "com.example.kiosk.mtls",
AccessGroup = "ABCDE12345.com.example.kiosk" // 共有が必要なら指定
};
SecStatusCode status;
var identity = SecKeyChain.QueryAsConcreteType(query, out status) as SecIdentity;
if (identity == null)
throw new InvalidOperationException($"Client certificate not found: {status}");
// SecIdentity → X509Certificate2 に変換(擬似コード)
SecCertificate certRef;
identity.TryGetCertificate(out certRef);
var cert = new X509Certificate2(certRef.ToX509Certificate());
// 非エクスポートの秘密鍵を保持したまま利用
handler.ClientCertificates.Add(cert);
#endif
注:実装では iOS の Security フレームワークバインディングを利用します。秘密鍵は端末外へ取り出さず、Keychain 参照で使用するのが原則です。
サーバー証明書のピンニング
static bool IsPinned(X509Certificate2 serverCert)
{
// 例:SPKI(公開鍵)の SHA-256 を Base64 で比較
using var sha = System.Security.Cryptography.SHA256.Create();
var spki = serverCert.GetPublicKey();
var hash = sha.ComputeHash(spki);
var fp = "sha256/" + Convert.ToBase64String(hash);
// 許可するフィンガープリントのリスト(ロールリング対応で複数保持)
var allowed = new[]
{
"sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=",
"sha256/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB="
};
return allowed.Contains(fp, StringComparer.Ordinal);
}
証明書更新に備え、重複期間を設けて複数フィンガープリントを保持します。
オフライン耐性(キオスク特有の要件)
- 書き込み系は永続キュー(例:SQLite + 専用キュー)へ格納し、回線復旧後に同期。
- 重複送信防止のためリクエストごとに
Idempotency-Keyを付与。 - 「この操作はオンライン必須」の判定と UI 明示(失敗時のガイダンス)。
API ゲートウェイ/WAF:入口で止める
推奨する防御レイヤ
| レイヤ | 機能 | 意図 |
|---|---|---|
| mTLS 終端 | クライアント証明書検証、CRL/OCSP、チェーン検証 | 未認証端末を物理的に遮断 |
| レート制限 | 端末/IP/サブネット単位の呼び出し回数制御 | DoS・暴走バグの影響を局所化 |
| Bot/ASN 制御 | 既知のデータセンター ASN からの通信遮断 | スクリプト攻撃の低減 |
| ヘッダ正規化 | 危険ヘッダの除去、X-Client-Cert の安全な付与 | 偽装・注入対策 |
| 監査 | 証明書サマリ(拇印/SAN)・IP・User-Agent を記録 | 事後調査・異常検知 |
ASP.NET Core 側の実装ポイント
Kestrel でクライアント証明書を要求
using Microsoft.AspNetCore.Server.Kestrel.Https;
using System.Security.Cryptography.X509Certificates;
var builder = WebApplication.CreateBuilder(args);
builder.WebHost.ConfigureKestrel(options =>
{
options.ConfigureHttpsDefaults(https =>
{
https.ClientCertificateMode = ClientCertificateMode.RequireCertificate;
https.ClientCertificateValidation = (cert, chain, errors) =>
{
// 1) チェーン・失効・期限などの通常検証
var valid = errors == SslPolicyErrors.None;
// 2) ポリシー検証(例:SAN の OU=Kiosk, Dept=Retail)
valid &= ValidateDeviceCertificate(cert);
return valid;
};
});
});
static bool ValidateDeviceCertificate(X509Certificate2 cert)
{
// 例:サブジェクト/拡張に含めた属性でデバイス群を制御
var subject = cert.Subject;
if (!subject.Contains("OU=Kiosk")) return false;
// さらに拇印・発行 CA・KeyUsage などで厳格化
return true;
}
リバースプロキシ配下での証明書フォワーディング
ゲートウェイで mTLS を終端する構成では、バックエンドはフォワードされた証明書を受け取って再検証します。
// Program.cs
builder.Services.AddCertificateForwarding(options =>
{
options.CertificateHeader = "X-Client-Cert"; // ゲートウェイ内でのみ付与
options.HeaderConverter = (headerValue) =>
{
// PEM または URL エスケープ済み証明書を X509Certificate2 へ
var pem = Uri.UnescapeDataString(headerValue);
var raw = ExtractDerFromPem(pem); // 実装は省略
return new X509Certificate2(raw);
};
});
builder.Services
.AddAuthentication(CertificateAuthenticationDefaults.AuthenticationScheme)
.AddCertificate();
var app = builder.Build();
app.UseCertificateForwarding();
app.UseAuthentication();
app.UseAuthorization();
app.MapGet("/health", () => "ok");
app.Run();
重要:外部からの X-Client-Cert ヘッダは必ずゲートウェイで除去してください。バックエンドはゲートウェイの内側からの通信のみ許可します。
権限制御とスコープの切り分け
- 証明書の OU/カスタム OID で機能スコープ(例:
ReadOnly/Full)を切り分け。 - 最小権限の原則:書き込み系 API は別ホスト名・別証明書グループで分離可能。
証明書ライフサイクル運用
自動ローテーションと失効
- 有効期限は短め(180~365 日)。期限の 1/3 経過で自動更新を開始。
- 端末紛失・退役・疑い発生時は即失効(MDM/CA から CRL/OCSP に反映)。
- ロールリングに備え、クライアントは複数のサーバーフィンガープリントを保持。
トレーサビリティ
- 監査ログに 証明書拇印・発行 CA・SAN 情報・MDM 端末 ID・IP・ASN を記録。
- 異常行動(レート超過・時刻ずれ・地域外アクセス)に対し自動遮断ルール。
アプリ改ざん・脱獄対策
- 証明書ピンニング:中間者攻撃を抑止。更新時は重複期間を設ける。
- 脱獄検知:既知パスの存在、システムコール異常、署名検証などの複合判定。
- 整合性チェック:アプリ実行時にビルド署名やハッシュの検証を追加。
- デバッグ禁止:本番ビルドで JIT/デバッグアタッチを抑止。
これらは“完全防御”ではありませんが、攻撃者のコストを大幅に引き上げ、量的攻撃を現実的でない水準へ引き上げます。
キオスク現場に寄せた運用チェックリスト
| 項目 | チェック内容 | 頻度 | 自動化 |
|---|---|---|---|
| Single App Mode | 全端末で適用・逸脱なし | 常時監視 | MDM レポート |
| 証明書期限 | 30 日未満の端末一覧と自動更新率 | 毎日 | MDM + CA |
| 失効反映 | CRL/OCSP の遅延なし | 毎時 | 監視ツール |
| レート制限 | 端末ごとの上限超過アラート | 随時 | ゲートウェイ |
| 改ざん・脱獄 | 検知時の自動隔離(ネットワーク遮断) | 随時 | MDM スクリプト |
| 更新ドリフト | OS/アプリのバージョン差分 | 毎週 | MDM レポート |
想定攻撃と対策の対応表
| 攻撃/リスク | 説明 | 対策 |
|---|---|---|
| API キーの盗難 | キー埋め込みクライアントの逆コンパイル | API キーは不使用。mTLS と端末証明書のみで認証 |
| クレデンシャル詐取 | フィッシング等 | そもそもユーザー認証なし=攻撃面を消す |
| MITM | プロキシ差し替え・社内 CA 偽装 | 証明書ピンニング、HSTS、TLS1.2+ |
| スクリプトからの直叩き | curl 等で API を直接攻撃 | mTLS 必須、ゲートウェイで未提示は TCP 拒否 |
| 証明書漏洩 | 端末紛失・バックアップから流出 | 非エクスポート鍵、短寿命、即失効、WAF の拇印ブロック |
| DoS/ブルートフォース | 過剰リクエスト | レート制限、バースト制限、IP/ASN ブロック |
実装スニペット集
NGINX で mTLS 終端し、証明書を転送
server {
listen 443 ssl http2;
ssl_certificate /etc/nginx/server.pem;
ssl_certificate_key /etc/nginx/server.key;
# mTLS
ssl_client_certificate /etc/nginx/ca_client.pem;
ssl_verify_client on;
# フロントからの偽装ヘッダを除去
more_clear_headers "X-Client-Cert";
# バックエンドに証明書を安全に引き渡し
proxy_set_header X-Client-Cert $ssl_client_escaped_cert;
location / {
proxy_pass http://backend;
# レート制限等は省略
}
}
ASP.NET Core:証明書属性による権限分岐(例)
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("KioskOnly", policy =>
policy.RequireAssertion(ctx =>
{
var cert = ctx.User?.FindFirst("x5t")?.Value; // 例:拇印をクレーム化
var ou = ctx.User?.FindFirst("cert.ou")?.Value; // 例:変換済みクレーム
return ou == "Kiosk" && IsTrustedThumbprint(cert);
}));
});
エラー応答の設計
- 401/403 のみ:詳細理由は返さず、内部ログに十分なメタ情報を保持。
- 429(レート超過):
Retry-Afterを返し、アプリ側は指数バックオフ。 - 5xx は最小化:ゲートウェイでのサーキットブレークにより保護。
オフライン設計の具体化
キオスクは回線品質の揺らぎに晒されます。書き込みイベントは必ず永続化し、同期プロセスを分離します。
- UI スレッドと送信スレッドを分離。
- 各リクエストに再送可能な識別子(
Idempotency-Key)を付与。 - 送信結果(成功/重複/失敗)をキューへ反映。
- 期限切れ・サイズ超過のキューはロールリング。
ゼロトラストの観点での強化策
- Per-App VPN:対象アプリの通信のみ社内経由に強制(MDM で構成)。
- ネットワーク分離:管理プレーン(MDM/CA)とデータプレーン(API)を物理・論理的に分離。
- 観測性:分散トレースに証明書拇印を埋め込み、端末単位の SLO を可視化。
よくある落とし穴
- API キー依存:クライアントへ埋め込むキーは即漏れる。mTLS に置き換える。
- Guided Access の過信:解除手順が現場に広まりやすい。Single App Mode で強制。
- 長寿命証明書:運用が楽なほど事故時の影響が長引く。短寿命+自動更新が正解。
- ピンニングの単一点:更新で全滅する。並行稼働期間を設け、複数キーを許可。
- ヘッダ偽装:
X-Client-Certを外部から通してしまう構成ミスに注意。
サンプル構成テンプレート(最小構成)
| 層 | 構成 | ポイント |
|---|---|---|
| 端末 | iPad + Single App Mode + MDM | キオスク固定、証明書配布、OS/アプリ更新 |
| アプリ | .NET MAUI + mTLS + ピンニング | Keychain の非エクスポート鍵を使用 |
| フロント | API ゲートウェイ/WAF | mTLS 終端、レート制限、失効チェック |
| バックエンド | ASP.NET Core API | 証明書再検証、ポリシー制御、監査 |
| PKI | 社内 CA / MDM(SCEP) | 短寿命・自動ローテーション・即時失効 |
導入から運用までのステップ
- PKI 設計:ルート/中間 CA、証明書テンプレート(SAN/OU/KeyUsage)確定。
- MDM 準備:Single App Mode、証明書配布プロファイル、Per-App VPN(必要時)。
- ゲートウェイ:mTLS 必須化、レート制限、危険ヘッダ除去、監査フォーマット。
- API:証明書再検証、権限分岐、監査ログ、ヘルスチェック。
- MAUI:Keychain 参照、ピンニング、オフラインキュー、再試行。
- 運用:期限監視、失効即時反映、逸脱検知、自動隔離。
サンプルコード:ASP.NET Core での厳格検証
builder.Services
.AddAuthentication(CertificateAuthenticationDefaults.AuthenticationScheme)
.AddCertificate(options =>
{
options.ChainTrustValidationMode = X509ChainTrustMode.System;
options.Events = new CertificateAuthenticationEvents
{
OnCertificateValidated = context =>
{
var cert = context.ClientCertificate;
if (!ValidateDeviceCertificate(cert))
{
context.Fail("Device certificate policy mismatch.");
return Task.CompletedTask;
}
// クレーム付与例(拇印・OU など)
var claims = new List<Claim>
{
new("x5t", cert.Thumbprint ?? string.Empty),
new("cert.subject", cert.Subject),
};
var id = new ClaimsIdentity(claims, context.Scheme.Name);
context.Principal = new ClaimsPrincipal(id);
context.Success();
return Task.CompletedTask;
}
};
});
トラブルシューティング
- ハンドシェイクで失敗:ゲートウェイの CA バンドル不一致/クライアント証明書の KeyUsage 未設定。
- iOS 側で証明書が選ばれない:アイデンティティが Keychain に存在しない、またはアクセスグループ不一致。
- ピンニングで拒否:SPKI 指紋が更新されていない。並行稼働期間を設けて双方に登録。
- 失効が反映されない:CRL キャッシュや OCSP ステープリングの遅延。TTL と更新フローを再設計。
セキュリティと UX の両立
キオスクでは「人に優しい」ことが最終品質です。セキュリティが強くても、現場で止まりやすい設計は避けるべきです。以下の原則が役立ちます。
- Fail-Closed / Recover-Fast:怪しい通信は即座に閉じるが、復旧動線(再試行・キュー同期)は素早く。
- ノイズの少ない監視:レート制限や失敗率の閾値を運用実態に合わせチューニング。
- 現場用ダッシュボード:端末単位の接続状態/証明書期限/アプリ版数を色分け表示。
まとめ
- ユーザー認証を捨てるなら、端末証明書+キオスク固定=デバイス認証を最優先に据える。
- mTLS + API ゲートウェイ + MDM で「正規端末以外は API に到達すらできない」状態を標準に。
- 証明書の配布・失効・更新、改ざん検知、オフライン同期までを運用に織り込み、設計・実装・運用の三層で穴を塞ぐ。
付録:最小サンプル(MAUI 側)
public static HttpClient CreatePinnedMtlsHttpClient(X509Certificate2 clientCert, string[] allowedSpkiHashes)
{
bool IsPinned(X509Certificate2 serverCert)
{
using var sha = System.Security.Cryptography.SHA256.Create();
var spki = serverCert.GetPublicKey();
var fp = "sha256/" + Convert.ToBase64String(sha.ComputeHash(spki));
return allowedSpkiHashes.Contains(fp, StringComparer.Ordinal);
}
var handler = new HttpClientHandler
{
ClientCertificates = { clientCert },
ServerCertificateCustomValidationCallback = (req, cert, chain, errors) =>
errors == System.Net.Security.SslPolicyErrors.None && IsPinned(cert)
};
return new HttpClient(handler)
{
Timeout = TimeSpan.FromSeconds(30)
};
}
付録:運用 Runbook 雛形
- 端末紛失時:MDM でデバイスワイプ → 証明書失効 → ゲートウェイで拇印ブロック → 監査。
- 証明書更新失敗:MDM のプロファイル再配布 → 端末再起動 → ログ収集。
- 大量 403/401:ゲートウェイの CRL/OCSP 状態確認 → 直近の CA/中間更新を確認。
- レート制限多発:クライアントのリトライ設定見直し(指数バックオフ・ジッタ付与)。
質問の要約と実践解
- 端末認証に切り替える:ユーザーではなく「MDM 管理下の iPad とアプリ」を信頼主体に。
- iPad をキオスク固定:Single App Mode を MDM で適用、Guided Access は避ける。
- mTLS を必須化:クライアント証明書未提示の通信は接続不可。ゲートウェイで失効チェック。
- API ゲートウェイ/WAF:レート制限、IP/Bot 制御、危険ヘッダ除去、監査。
- 証明書ライフサイクル:短寿命+自動更新、紛失時は即失効。
- アプリ改ざん対策:証明書ピンニング、脱獄検知、整合性チェック。
.NET MAUI での mTLS 概要コード
var handler = new HttpClientHandler
{
ClientCertificates = { certFromKeyChain },
ServerCertificateCustomValidationCallback = (req, cert, chain, errors) =>
errors == System.Net.Security.SslPolicyErrors.None && IsPinned(cert)
};
var client = new HttpClient(handler);
ASP.NET Core(Kestrel)での要求設定
builder.WebHost.ConfigureKestrel(options =>
{
options.ConfigureHttpsDefaults(https =>
{
https.ClientCertificateMode = ClientCertificateMode.RequireCertificate;
https.ClientCertificateValidation = (cert, chain, errors) => ValidateDeviceCertificate(cert);
});
});
オフライン時の挙動
キャッシュ不可の操作は「再接続時の同期」フローを実装。オンライン必須業務なら回線冗長化(回線二重化・セルラー/有線切替・帯域監視)を計画に含めます。
最終メッセージ:ユーザー認証を排したキオスクでも、端末・通信・入口の三点を固めれば、攻撃面を最小化できます。mTLS と MDM を核に、ゲートウェイ/WAF で入口を固め、証明書とアプリの運用を仕組み化しましょう。

コメント