メインコンテンツまでスキップ

OPA (Open Policy Agent) による認可

Open Policy Agent (OPA) は、統一的なポリシーベースの認可を実現するオープンソースのポリシーエンジンです。

OPA とは

OPA(オーパ)は、Cloud Native Computing Foundation (CNCF) のプロジェクトで、アプリケーション全体でポリシーを統一的に管理・適用するためのツールです。

主な特徴

  1. ポリシーの外部化

    • アプリケーションコードからポリシー決定ロジックを分離
    • ポリシーの変更にコード修正が不要
  2. 汎用性

    • マイクロサービス、Kubernetes、API Gateway など様々な環境で利用可能
    • 言語やフレームワークに依存しない
  3. 宣言的なポリシー言語 (Rego)

    • 専用の高水準言語でポリシーを記述
    • 可読性が高く、複雑なルールも表現可能
  4. リアルタイムな意思決定

    • ミリ秒単位での高速なポリシー評価
    • 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 の利点と欠点

利点

  1. ポリシーの一元管理

    • 複数のアプリケーションで統一的なポリシーを適用
    • ポリシーの変更が容易
  2. 柔軟性

    • RBAC、ABAC、その他の認可モデルを柔軟に実装
    • 複雑なビジネスルールを表現可能
  3. テスタビリティ

    • ポリシーを独立してテスト可能
    • ユニットテストフレームワークが提供されている
  4. 言語非依存

    • どのプログラミング言語からでも利用可能
    • REST API 経由で統合
  5. 高パフォーマンス

    • インメモリでの高速な評価
    • ミリ秒単位での応答

欠点

  1. 学習コスト

    • Rego 言語の習得が必要
    • 宣言的プログラミングに慣れる必要がある
  2. デバッグの難しさ

    • 複雑なポリシーのデバッグが困難な場合がある
    • トレース機能を活用する必要がある
  3. 運用コスト

    • OPA サービスの管理・監視が必要
    • 高可用性の確保にコストがかかる
  4. ステートレス

    • OPA 自体はステートレスなため、動的データは外部から提供する必要がある
    • データの同期が課題になる場合がある

まとめ

  • OPA はポリシーベースの認可を実現する汎用ポリシーエンジン
  • Rego 言語で宣言的にポリシーを記述
  • マイクロサービス、Kubernetes、API Gateway など様々な環境で利用可能
  • デフォルト拒否の原則とテスタブルなポリシー設計が重要
  • 認証(Authentication)と組み合わせることで、強固なセキュリティを実現

参考リンク