外部に公開してはならないAPI endpointの対処法

システム開発において、特定のAPIを外部ネットワークに公開せず、内部サービス間でのみ利用できるようにしたい場合、代表的な3つのアプローチが存在する。

  1. 内部向け・外部向けでマイクロサービスを分離する
  2. ゲートウェイとRedisを活用したホワイトリスト方式
  3. ゲートウェイとAOPによるリクエスト元判定

以下で、それぞれの方案の特徴と実装方法について詳しく解説する。

1. 内部向け・外部向けでマイクロサービスを分離する

外部に公開するAPIと内部でのみ利用するAPIを、それぞれ独立したマイクロサービスとして構築する手法である。一方のサービスには外部公開用APIを集約し、もう一方のサービスには内部サービス間専用のAPI만을実装する。

この方式では、新規に内部専用マイクロサービスを作成する必要があり、そこに内部限定ビジネスロジックを集約する。必要に応じて各ビジネスドメインからリソースを取得し、レスポンスを返却する構成となる。

ただし、別途マイクロサービスを用意することでシステム構成が複雑化し、レイテンシの増加や保守コストの増大という課題が生じる。サービス数が増加することで、運用管理の負荷も高まる点が懸念事項となる。

2. ゲートウェイとRedisを活用したホワイトリスト方式

Redis上にAPIのホワイトリストを保持し、外部からのリクエストがゲートウェイに到達した段階でRedisからリストを取得、許可されたAPIのみを通行拒否するという手法である。

この方式の利点は、ビジネスロジックへの影響をゼロに抑えられる点であり、ホワイトリストの管理さえ適切に行えば既存のコードに変更を加える必要がない。

一方で、ホワイトリストの維持管理は継続的な作業となる。多くの組織ではビジネス開発チームがRedisに直接アクセスできないため、工数申請を通じて更新を行う必要があり、これが開発コストの増大につながる。また、全リクエストに対してホワイトリスト判定が発生するため、レイテンシへの影響も無視できない。通常、外部からのリクエストの大半は正当なものであり、ホワイトリストによってブロックされるのは悪意あるリクエストのみであることを考慮すると、費用対効果の観点から推奨しにくい方式である。

3. ゲートウェイとAOPによるリクエスト元判定

方案二がAPI単位でのホワイトリスト判定を行うのに対し、方案三はリクエスト元の判定をビジネスロジック層まで押し下げる手法である。これにより、ゲートウェイでの判定処理を排除し、システム全体の応答速度を向上させることができる。

外部からのリクエストは必ずゲートウェイを経由してからビジネスロジックに到達するのに対し、内部サービス間の通信はKubernetesのserviceを経由するためゲートウェイを経由しない。この特性を利用することで、リクエストの種別を明確に判別可能となる。

具体的な実装としては、ゲートウェイを経由するリクエストのヘッダーに識別情報を付与し、ビジネスロジック側でこのヘッダーを検証することで、リクエスト元が外部か内部かを判定する。この方式では、ゲートウェイでの処理を分散化し、スループットのボトルネックを解消できる。また、開発者がビジネスロジック内で直接アクセス権限を制御できるため、コードの可読性が向上し、開発効率も高まる。

ビジネスロジックへの一定の侵入が生じる点は否めないが、アノテーションを活用することで影響を最小限に抑えることができる。以下に具体的な実装例を示す。

ゲートウェイフィルタの実装:

@Component
public class RequestOriginMarkerFilter implements GlobalFilter, Ordered {
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        ServerHttpRequest mutatedRequest = request.mutate()
                .header("X-Request-Source", "external")
                .header("X-Forwarded-For", "gateway")
                .build();
        
        return chain.filter(exchange.mutate().request(mutatedRequest).build());
    }

    @Override
    public int getOrder() {
        return Ordered.HIGHEST_PRECEDENCE;
    }
}

リクエスト元を判定するAOPクラスとアノテーションの実装:

@Aspect
@Component
public class InternalOnlyEnforcementAspect {
    private static final Logger logger = LoggerFactory.getLogger(InternalOnlyEnforcementAspect.class);
    private static final String EXTERNAL_SOURCE = "external";

    @Pointcut("@within(com.example.security.InternalOnly) || @annotation(com.example.security.InternalOnly)")
    public void internalOnlyEndpoints() {}

    @Before("internalOnlyEndpoints()")
    public void validateRequestOrigin() {
        HttpServletRequest httpRequest = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        String requestSource = httpRequest.getHeader("X-Request-Source");
        
        if (!StringUtils.isEmpty(requestSource) && EXTERNAL_SOURCE.equals(requestSource)) {
            logger.warn("外部からのアクセスが検出されました。対象endpoint: {}", httpRequest.getRequestURI());
            throw new AccessDeniedException("このAPIは内部ネットワークからのみアクセス可能です");
        }
    }
}

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface InternalOnly {
}

対象となるAPIへの適用:

@RestController
@RequestMapping("/api/v1/admin")
public class AdminController {
    
    @GetMapping("/users")
    @InternalOnly
    public ResponseEntity<UserListResponse> getUserList() {
        return ResponseEntity.ok(userService.fetchAllUsers());
    }

    @PostMapping("/roles")
    @InternalOnly
    public ResponseEntity<RoleResponse> createRole(@RequestBody RoleCreateRequest request) {
        return ResponseEntity.ok(roleService.registerNewRole(request));
    }
}

この実装により、内部専用のAPIには@InternalOnlyアノテーションを付与するだけで、外部からの不正アクセスを自動的に遮断できる。ゲートウェイでの共通処理とビジネスロジックでの細やかなアクセス制御を組み合わせることで、セキュアかつ効率的なシステム構築が実現可能となる。

タグ: Spring Cloud Gateway redis AOP Spring Boot マイクロサービス

5月24日 05:00 投稿