OPA (Open Policy Agent) による認可
Open Policy Agent (OPA) は、統一的なポリシーベースの認可を実現するオープンソースのポリシーエンジンです。
OPA とは
OPA(オーパ)は、Cloud Native Computing Foundation (CNCF) のプロジェクトで、アプリケーション全体でポリシーを統一的に管理・適用するためのツールです。
主な特徴
-
ポリシーの外部化
- アプリケーションコードからポリシー決定ロジックを分離
- ポリシーの変更にコード修正が不要
-
汎用性
- マイクロサービス、Kubernetes、API Gateway など様々な環境で利用可能
- 言語やフレームワークに依存しない
-
宣言的なポリシー言語 (Rego)
- 専用の高水準言語でポリシーを記述
- 可読性が高く、複雑なルールも表現可能
-
リアルタイムな意思決定
- ミリ秒単位での高速なポリシー評価
- API として利用可能
OPA のアーキテクチャ
OPA の基本的な動作フロー
Rego ポリシー言語
Rego は OPA で使用される宣言的なポリシー言語です。
基本的な文法
シンプルな認可ポリシー
package authz
# デフォルトでは拒否
default allow = false
# 管理者は常に許可
allow {
input.user.role == "admin"
}
# 一般ユーザーは自分のリソースのみ読み取り可能
allow {
input.user.role == "user"
input.action == "read"
input.resource.owner == input.user.id
}
ポリシーの構成要素:
package: ポリシーの名前空間default: デフォルト値の設定allow: 許可判定のルールinput: アプリケーションから渡されるコンテキスト情報
より複雑な例:RBAC (Role-Based Access Control)
package rbac
import future.keywords.if
import future.keywords.in
# デフォルトは拒否
default allow = false
# 管理者は全てのアクションが可能
allow if {
input.user.role == "admin"
}
# エディターは作成・読み取り・更新が可能
allow if {
input.user.role == "editor"
input.action in ["create", "read", "update"]
}
# ビューワーは読み取りのみ可能
allow if {
input.user.role == "viewer"
input.action == "read"
}
# リソースオーナーは自分のリソースを完全制御
allow if {
input.resource.owner == input.user.id
input.action in ["read", "update", "delete"]
}
ABAC (Attribute-Based Access Control) の例
package abac
import future.keywords.if
import future.keywords.in
default allow = false
# ユーザーが所属する部門のリソースにアクセス可能
allow if {
input.user.department == input.resource.department
input.action == "read"
}
# プロジェクトメンバーは編集可能
allow if {
input.user.id in input.resource.project_members
input.action in ["read", "update"]
}
# 勤務時間内のみアクセス許可
allow if {
input.user.role == "employee"
is_business_hours
}
is_business_hours if {
hour := time.clock(time.now_ns())[0]
hour >= 9
hour < 18
}
# 機密データは特定の認証レベル以上のユーザーのみ
allow if {
input.resource.classification == "confidential"
input.user.clearance_level >= 3
input.action == "read"
}
実装例
ASP.NET Core での統合
OPA のセットアップ
# Docker で OPA を起動
docker run -p 8181:8181 openpolicyagent/opa:latest \
run --server --log-level debug
ポリシーファイルの配置 (authz.rego)
package authz
import future.keywords.if
import future.keywords.in
default allow = false
# 管理者は全てのリソースにアクセス可能
allow if {
input.user.role == "admin"
}
# ユーザーは自分のデータにのみアクセス可能
allow if {
input.user.role == "user"
input.action in ["read", "update"]
input.resource.userId == input.user.id
}
# 公開リソースは誰でも読み取り可能
allow if {
input.action == "read"
input.resource.visibility == "public"
}
OPA クライアントサービスの実装
using System.Text;
using System.Text.Json;
public interface IOpaClient
{
Task<bool> EvaluateAsync(object input);
}
public class OpaClient : IOpaClient
{
private readonly HttpClient _httpClient;
private readonly ILogger<OpaClient> _logger;
private const string OpaUrl = "http://localhost:8181/v1/data/authz/allow";
public OpaClient(HttpClient httpClient, ILogger<OpaClient> logger)
{
_httpClient = httpClient;
_logger = logger;
}
public async Task<bool> EvaluateAsync(object input)
{
try
{
var payload = new { input };
var json = JsonSerializer.Serialize(payload);
var content = new StringContent(json, Encoding.UTF8, "application/json");
var response = await _httpClient.PostAsync(OpaUrl, content);
response.EnsureSuccessStatusCode();
var responseBody = await response.Content.ReadAsStringAsync();
var result = JsonSerializer.Deserialize<OpaResponse>(responseBody);
return result?.Result ?? false;
}
catch (Exception ex)
{
_logger.LogError(ex, "OPA evaluation error");
throw new InvalidOperationException("Authorization service unavailable", ex);
}
}
private class OpaResponse
{
public bool Result { get; set; }
}
}
認可ミドルウェアの実装
public class OpaAuthorizationMiddleware
{
private readonly RequestDelegate _next;
private readonly IOpaClient _opaClient;
private readonly ILogger<OpaAuthorizationMiddleware> _logger;
public OpaAuthorizationMiddleware(
RequestDelegate next,
IOpaClient opaClient,
ILogger<OpaAuthorizationMiddleware> logger)
{
_next = next;
_opaClient = opaClient;
_logger = logger;
}
public async Task InvokeAsync(HttpContext context)
{
// 認証済みユーザー情報を取得
if (!context.User.Identity?.IsAuthenticated ?? true)
{
context.Response.StatusCode = StatusCodes.Status401Unauthorized;
await context.Response.WriteAsJsonAsync(new { error = "Authentication required" });
return;
}
var userId = context.User.FindFirst("sub")?.Value;
var role = context.User.FindFirst("role")?.Value;
var action = GetActionFromMethod(context.Request.Method);
// OPA への入力データを構築
var input = new
{
user = new
{
id = userId,
role = role
},
action = action,
resource = new
{
userId = context.Request.RouteValues["userId"]?.ToString(),
visibility = context.Items["ResourceVisibility"]?.ToString() ?? "private"
}
};
try
{
var allowed = await _opaClient.EvaluateAsync(input);
if (allowed)
{
await _next(context);
}
else
{
_logger.LogWarning(
"Access denied for user {UserId} to perform {Action}",
userId, action);
context.Response.StatusCode = StatusCodes.Status403Forbidden;
await context.Response.WriteAsJsonAsync(new { error = "Access denied" });
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Authorization error");
context.Response.StatusCode = StatusCodes.Status500InternalServerError;
await context.Response.WriteAsJsonAsync(new { error = "Authorization service error" });
}
}
private static string GetActionFromMethod(string method)
{
return method switch
{
"GET" => "read",
"POST" => "create",
"PUT" => "update",
"PATCH" => "update",
"DELETE" => "delete",
_ => "unknown"
};
}
}
Program.cs での設定
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using System.Text;
var builder = WebApplication.CreateBuilder(args);
// OPA クライアントの登録
builder.Services.AddHttpClient<IOpaClient, OpaClient>();
// JWT 認証の設定
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = builder.Configuration["Jwt:Issuer"],
ValidAudience = builder.Configuration["Jwt:Audience"],
IssuerSigningKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"] ?? string.Empty)
)
};
});
builder.Services.AddAuthorization();
var app = builder.Build();
app.UseAuthentication();
// OPA 認可ミドルウェアを適用
app.UseMiddleware<OpaAuthorizationMiddleware>();
app.UseAuthorization();
// 保護されたエンドポイント
app.MapGet("/api/users/{userId}/profile", (string userId, HttpContext context) =>
{
return Results.Ok(new { message = "User profile data", userId });
})
.RequireAuthorization();
app.MapPut("/api/users/{userId}/profile", (string userId, HttpContext context) =>
{
return Results.Ok(new { message = "Profile updated", userId });
})
.RequireAuthorization();
app.MapGet("/api/resources/{resourceId}", async (
string resourceId,
HttpContext context,
IResourceRepository repository) =>
{
var resource = await repository.GetByIdAsync(resourceId);
if (resource == null)
{
return Results.NotFound(new { error = "Resource not found" });
}
// リソース情報をコンテキストに追加(OPA ミドルウェアで使用)
context.Items["ResourceVisibility"] = resource.Visibility;
return Results.Ok(resource);
})
.RequireAuthorization();
app.Run();
OPA ポリシーの動的ロード
public class OpaPolicyService
{
private readonly HttpClient _httpClient;
private readonly ILogger<OpaolicyService> _logger;
public OpaolicyService(HttpClient httpClient, ILogger<OpaolicyService> logger)
{
_httpClient = httpClient;
_logger = logger;
}
public async Task UploadPolicyAsync(string policyPath)
{
try
{
var policy = await File.ReadAllTextAsync(policyPath);
var content = new StringContent(policy, Encoding.UTF8, "text/plain");
var response = await _httpClient.PutAsync(
"http://localhost:8181/v1/policies/authz",
content);
response.EnsureSuccessStatusCode();
_logger.LogInformation("Policy uploaded successfully");
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to upload policy");
throw;
}
}
}
// アプリケーション起動時にポリシーをロード
public class PolicyLoaderHostedService : IHostedService
{
private readonly OpaolicyService _policyService;
public PolicyLoaderHostedService(OpaolicyService policyService)
{
_policyService = policyService;
}
public async Task StartAsync(CancellationToken cancellationToken)
{
await _policyService.UploadPolicyAsync("./policies/authz.rego");
}
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
}
属性ベースの認可実装
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class OpaAuthorizeAttribute : Attribute, IAsyncAuthorizationFilter
{
private readonly string _action;
public OpaAuthorizeAttribute(string action)
{
_action = action;
}
public async Task OnAuthorizationAsync(AuthorizationFilterContext context)
{
var opaClient = context.HttpContext.RequestServices
.GetRequiredService<IOpaClient>();
var userId = context.HttpContext.User.FindFirst("sub")?.Value;
var role = context.HttpContext.User.FindFirst("role")?.Value;
var input = new
{
user = new { id = userId, role = role },
action = _action,
resource = new
{
userId = context.RouteData.Values["userId"]?.ToString()
}
};
var allowed = await opaClient.EvaluateAsync(input);
if (!allowed)
{
context.Result = new ForbidResult();
}
}
}
// 使用例
[ApiController]
[Route("api/users")]
public class UsersController : ControllerBase
{
[HttpGet("{userId}/profile")]
[OpaAuthorize("read")]
public IActionResult GetProfile(string userId)
{
return Ok(new { message = "User profile data", userId });
}
[HttpPut("{userId}/profile")]
[OpaAuthorize("update")]
public IActionResult UpdateProfile(string userId)
{
return Ok(new { message = "Profile updated", userId });
}
}
Kubernetes での利用
OPA は Kubernetes のアドミッションコントローラーとして利用できます。
Gatekeeper のインストール
# Gatekeeper (OPA for Kubernetes) をインストール
kubectl apply -f https://raw.githubusercontent.com/open-policy-agent/gatekeeper/release-3.15/deploy/gatekeeper.yaml
Constraint Template の定義
apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
name: k8srequiredlabels
spec:
crd:
spec:
names:
kind: K8sRequiredLabels
validation:
openAPIV3Schema:
type: object
properties:
labels:
type: array
items:
type: string
targets:
- target: admission.k8s.gatekeeper.sh
rego: |
package k8srequiredlabels
violation[{"msg": msg, "details": {"missing_labels": missing}}] {
provided := {label | input.review.object.metadata.labels[label]}
required := {label | label := input.parameters.labels[_]}
missing := required - provided
count(missing) > 0
msg := sprintf("必須ラベルがありません: %v", [missing])
}
Constraint の適用
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sRequiredLabels
metadata:
name: require-app-labels
spec:
match:
kinds:
- apiGroups: ["apps"]
kinds: ["Deployment", "StatefulSet"]
parameters:
labels: ["app", "environment", "owner"]
API Gateway との統合
Envoy + OPA の例
# Envoy 設定
static_resources:
listeners:
- name: listener_0
address:
socket_address:
address: 0.0.0.0
port_value: 8080
filter_chains:
- filters:
- name: envoy.filters.network.http_connection_manager
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
stat_prefix: ingress_http
route_config:
name: local_route
virtual_hosts:
- name: backend
domains: ["*"]
routes:
- match:
prefix: "/"
route:
cluster: backend_service
http_filters:
- name: envoy.ext_authz
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.ext_authz.v3.ExtAuthz
grpc_service:
envoy_grpc:
cluster_name: opa
timeout: 0.5s
- name: envoy.filters.http.router
clusters:
- name: opa
type: STRICT_DNS
lb_policy: ROUND_ROBIN
load_assignment:
cluster_name: opa
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: opa
port_value: 9191
ベストプラクティス
1. ポリシーの設計原則
ポリシーの分離
# ベースポリシー
package base
default allow = false
# 管理機能のポリシー
package admin
import data.base
allow {
base.allow
input.user.role == "admin"
}
# ユーザー機能のポリシー
package user
import data.base
allow {
base.allow
input.user.role == "user"
input.resource.owner == input.user.id
}
テスタブルなポリシー
package authz_test
import data.authz
# 管理者のテスト
test_admin_can_access_all {
authz.allow with input as {
"user": {"role": "admin"},
"action": "delete",
"resource": {"owner": "other_user"}
}
}
# 一般ユーザーのテスト
test_user_can_access_own_resource {
authz.allow with input as {
"user": {"id": "user123", "role": "user"},
"action": "read",
"resource": {"owner": "user123"}
}
}
test_user_cannot_access_others_resource {
not authz.allow with input as {
"user": {"id": "user123", "role": "user"},
"action": "read",
"resource": {"owner": "other_user"}
}
}
2. パフォーマンス最適化
ポリシーのキャッシュ
using Microsoft.Extensions.Caching.Memory;
public class CachedOpaClient : IOpaClient
{
private readonly IOpaClient _innerClient;
private readonly IMemoryCache _cache;
private readonly ILogger<CachedOpaClient> _logger;
private readonly TimeSpan _cacheDuration = TimeSpan.FromMinutes(5);
public CachedOpaClient(
IOpaClient innerClient,
IMemoryCache cache,
ILogger<CachedOpaClient> logger)
{
_innerClient = innerClient;
_cache = cache;
_logger = logger;
}
public async Task<bool> EvaluateAsync(object input)
{
var cacheKey = GenerateCacheKey(input);
if (_cache.TryGetValue(cacheKey, out bool cachedResult))
{
_logger.LogDebug("Cache hit for key: {CacheKey}", cacheKey);
return cachedResult;
}
var result = await _innerClient.EvaluateAsync(input);
_cache.Set(cacheKey, result, _cacheDuration);
_logger.LogDebug("Cached result for key: {CacheKey}", cacheKey);
return result;
}
private static string GenerateCacheKey(object input)
{
return System.Text.Json.JsonSerializer.Serialize(input);
}
}
// Program.cs での登録
builder.Services.AddMemoryCache();
builder.Services.AddHttpClient<OpaClient>();
builder.Services.AddSingleton<IOpaClient, CachedOpaClient>(sp =>
{
var httpClient = sp.GetRequiredService<IHttpClientFactory>().CreateClient();
var logger = sp.GetRequiredService<ILogger<OpaClient>>();
var innerClient = new OpaClient(httpClient, logger);
var cache = sp.GetRequiredService<IMemoryCache>();
var cachedLogger = sp.GetRequiredService<ILogger<CachedOpaClient>>();
return new CachedOpaClient(innerClient, cache, cachedLogger);
});
バンドルの使用
OPA バンドルを使用して、ポリシーとデータを効率的に配布します。
# バンドルの作成
opa build -b ./policies -o bundle.tar.gz
# OPA をバンドルで起動
docker run -p 8181:8181 \
-v $(pwd)/bundle.tar.gz:/bundle.tar.gz \
openpolicyagent/opa:latest \
run --server --bundle /bundle.tar.gz
3. セキュリティ考慮事項
入力の検証
package authz
import future.keywords.if
# 入力の検証
valid_input {
is_string(input.user.id)
is_string(input.user.role)
is_string(input.action)
is_object(input.resource)
}
# 有効な入力の場合のみ評価
allow if {
valid_input
# 実際の認可ロジック
...
}
デフォルト拒否の原則
# 常にデフォルトは拒否
default allow = false
# 明示的な許可ルールのみで許可
allow {
# 具体的な条件
}
ログとモニタリング
public class OpaAuthorizationMiddleware
{
private readonly RequestDelegate _next;
private readonly IOpaClient _opaClient;
private readonly ILogger<OpaAuthorizationMiddleware> _logger;
public OpaAuthorizationMiddleware(
RequestDelegate next,
IOpaClient opaClient,
ILogger<OpaAuthorizationMiddleware> logger)
{
_next = next;
_opaClient = opaClient;
_logger = logger;
}
public async Task InvokeAsync(HttpContext context)
{
var stopwatch = System.Diagnostics.Stopwatch.StartNew();
try
{
var userId = context.User.FindFirst("sub")?.Value;
var role = context.User.FindFirst("role")?.Value;
var action = GetActionFromMethod(context.Request.Method);
var resourceId = context.Request.RouteValues["resourceId"]?.ToString();
var input = BuildOpaInput(context);
var result = await _opaClient.EvaluateAsync(input);
stopwatch.Stop();
// 構造化ログ記録
_logger.LogInformation(
"OPA Authorization - User: {UserId}, Action: {Action}, Resource: {ResourceId}, " +
"Allowed: {Allowed}, Duration: {Duration}ms",
userId, action, resourceId, result, stopwatch.ElapsedMilliseconds);
if (result)
{
await _next(context);
}
else
{
context.Response.StatusCode = StatusCodes.Status403Forbidden;
await context.Response.WriteAsJsonAsync(new { error = "Access denied" });
}
}
catch (Exception ex)
{
stopwatch.Stop();
_logger.LogError(ex,
"Authorization error after {Duration}ms",
stopwatch.ElapsedMilliseconds);
context.Response.StatusCode = StatusCodes.Status500InternalServerError;
await context.Response.WriteAsJsonAsync(new { error = "Internal server error" });
}
}
private object BuildOpaInput(HttpContext context)
{
var userId = context.User.FindFirst("sub")?.Value;
var role = context.User.FindFirst("role")?.Value;
var action = GetActionFromMethod(context.Request.Method);
return new
{
user = new { id = userId, role = role },
action = action,
resource = new
{
userId = context.Request.RouteValues["userId"]?.ToString(),
visibility = context.Items["ResourceVisibility"]?.ToString() ?? "private"
}
};
}
private static string GetActionFromMethod(string method)
{
return method switch
{
"GET" => "read",
"POST" => "create",
"PUT" => "update",
"PATCH" => "update",
"DELETE" => "delete",
_ => "unknown"
};
}
}
OPA の利点と欠点
利点
-
ポリシーの一元管理
- 複数のアプリケーションで統一的なポリシーを適用
- ポリシーの変更が容易
-
柔軟性
- RBAC、ABAC、その他の認可モデルを柔軟に実装
- 複雑なビジネスルールを表現可能
-
テスタビリティ
- ポリシーを独立してテスト可能
- ユニットテストフレームワークが提供されている
-
言語非依存
- どのプログラミング言語からでも利用可能
- REST API 経由で統合
-
高パフォーマンス
- インメモリでの高速な評価
- ミリ秒単位での応答
欠点
-
学習コスト
- Rego 言語の習得が必要
- 宣言的プログラミングに慣れる必要がある
-
デバッグの難しさ
- 複雑なポリシーのデバッグが困難な場合がある
- トレース機能を活用する必要がある
-
運用コスト
- OPA サービスの管理・監視が必要
- 高可用性の確保にコストがかかる
-
ステートレス
- OPA 自体はステートレスなため、動的データは外部から提供する必要がある
- データの同期が課題になる場合がある
まとめ
- OPA はポリシーベースの認可を実現する汎用ポリシーエンジン
- Rego 言語で宣言的にポリシーを記述
- マイクロサービス、Kubernetes、API Gateway など様々な環境で利用可能
- デフォルト拒否の原則とテスタブルなポリシー設計が重要
- 認証(Authentication)と組み合わせることで、強固なセキュリティを実現