SecretsもCognitoも不要!IAM発行JWTでSpring Bootマイクロサービス認証を実現する方法
はじめに
2025年11月、AWSはAWS IAM Outbound Identity Federation(STS の GetWebIdentityToken API)をリリースしました。これにより、AWSワークロードがJWTを使って自分のアイデンティティを外部サービスに安全に伝えられるようになりました。
以前、この機能で発行されたJWTを使ってAPI Gatewayで認証を行う方法を検証しました。今回は、ECS上で動作するSpring Bootアプリケーション間の通信をこの機能で保護する方法を検証します。
この実装で得られる効果
- シークレット管理不要: Client Secretの保管・ローテーション・監査が不要。漏洩リスクもゼロ
-
IAMベースのアクセス制御: ロール単位でアクセス権を柔軟に管理可能。IAM条件キー(
sts:IdentityTokenAudience、sts:SigningAlgorithm等)でトークン発行を細かく制御可能 - 追加リソース・コスト不要: Secrets Managerなどの追加リソース不要。本機能自体に追加料金は発生しません(ただし、ネットワーク転送やECS/ALBなど既存インフラの通常料金は別途発生)
- 標準的な実装: Spring Security OAuth2 Resource Serverに統合可能
アーキテクチャ概要
本実装では、ECS Fargate上で動作する2つのSpring Bootアプリケーションを構築します。
システム構成図
処理シーケンス図
構成のポイント:
実装のポイント
API Server側の実装
Spring Securityの枠組みの中で受信したトークンの検証を実現します。
SecurityConfig.java:
@Configuration
public class SecurityConfig {
private final AppSecurityProperties props;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(authorize -> authorize.anyRequest().authenticated())
.oauth2ResourceServer(oauth2 -> oauth2.jwt(jwt -> jwt.decoder(jwtDecoder())));
return http.build();
}
@Bean
public JwtDecoder jwtDecoder() {
NimbusJwtDecoder jwtDecoder = JwtDecoders.fromIssuerLocation(this.props.getIssuerUri());
OAuth2TokenValidator<Jwt> audienceValidator = new JwtAudienceValidator(this.props.getAllowedAudiences());
OAuth2TokenValidator<Jwt> withIssuer = JwtValidators.createDefaultWithIssuer(this.props.getIssuerUri());
OAuth2TokenValidator<Jwt> withAudience = new DelegatingOAuth2TokenValidator<>(withIssuer, audienceValidator);
jwtDecoder.setJwtValidator(withAudience);
return jwtDecoder;
}
}
JwtAudienceValidator.java:
public class JwtAudienceValidator implements OAuth2TokenValidator<Jwt> {
private final List<String> allowedAudiences;
public JwtAudienceValidator(List<String> allowedAudiences) {
this.allowedAudiences = allowedAudiences;
}
@Override
public OAuth2TokenValidatorResult validate(final Jwt token) {
List<String> tokenAudiences = token.getAudience();
for (String audience : tokenAudiences) {
if (allowedAudiences.contains(audience)) {
return OAuth2TokenValidatorResult.success();
}
}
return OAuth2TokenValidatorResult
.failure(new OAuth2Error("invalid_token", "The required audience is missing", null));
}
}
API Client側の実装
WebClientでリクエスト送信する際にAuthorizationヘッダにトークンを付与する典型的な通信方法を採用します。
STSWebIdentityTokenService.java:
@Service
public class STSWebIdentityTokenService {
@Autowired
private DownStreamProperties downStreamProperties;
private final StsClient stsClient;
public String getJwt() {
return this.stsClient.getWebIdentityToken(
GetWebIdentityTokenRequest.builder()
.audience(this.downStreamProperties.getAudience())
.signingAlgorithm("RS256")
.durationSeconds(60)
.build())
.webIdentityToken();
}
}
WebClientでの利用:
webClient.get()
.uri("/hello")
.header(HttpHeaders.AUTHORIZATION, "Bearer " + tokenService.getJwt())
.retrieve()
.bodyToMono(String.class);
インフラストラクチャ構成
AWS CDKで必要なリソースを定義します。
IAM権限の設定(条件付き):
const issuerUri = new AwsCustomResource(this, 'GetIamIssuerUri', {
onCreate: {
service: 'iam',
action: 'GetOutboundWebIdentityFederationInfo',
parameters: {},
physicalResourceId: PhysicalResourceId.fromResponse('IssuerIdentifier'),
},
policy: AwsCustomResourcePolicy.fromSdkCalls({ resources: AwsCustomResourcePolicy.ANY_RESOURCE }),
}).getResponseField('IssuerIdentifier');
const audience = 'com.mahitotsu.gekko002.api';
// 条件付きでトークン発行権限を付与
clientTaskDef.taskRole.addToPrincipalPolicy(new PolicyStatement({
effect: Effect.ALLOW,
actions: ['sts:GetWebIdentityToken'],
resources: ['*'],
conditions: {
'ForAnyValue:StringEquals': {
'sts:IdentityTokenAudience': [audience]
}
},
}));
アプリケーション設定の注入:
// サーバーコンテナ
serverTaskDef.addContainer('ServerContainer', {
image: ContainerImage.fromAsset('../api-server'),
environment: {
'APP_SECURITY_ISSUER_URI': issuerUri,
'APP_SECURITY_ALLOWED_AUDIENCES': audience
},
});
// クライアントコンテナ
clientTaskDef.addContainer('ClientContainer', {
image: ContainerImage.fromAsset('../api-client'),
environment: {
'DOWNSTREAM_ENDPOINT': `http://${serverName}.${localName}:${servicePort}/hello`,
'DOWNSTREAM_AUDIENCE': audience,
},
});
検証結果
検証の結果、狙っていた効果はすべて獲得できることが判明しました。
また、検証の過程で権限管理の柔軟さにより以下の追加の効果も確認できました。
- CloudShellでトークン発行してcurl等で動作確認が実現できた → テストやデバッグで採用可能な手法が多様化します
- AWSアカウント毎のIssuerがAPIで取得できた → IaCの実現が容易
TOKEN=$(aws sts get-web-identity-token \
--audience com.mahitotsu.gekko002.api \
--signing-algorithm RS256 \
--duration-seconds 60 \
--query 'WebIdentityToken' \
--output text)
curl -H "Authorization: Bearer $TOKEN" \
http://api-server.srv.local/hello
今後の可能性
Outbound Web Identity Tokenは、IAMロールの権限さえあれば簡単に取得できるため、さらなるユースケース拡張も期待できます。
- LambdaやEC2も容易にこのアクセス制御の仕組みに参加できる → API Gatewayが参加可能なのは検証済み
- CI/CDパイプライン内で実行されるテストスクリプトでもJWTが必要なトラフィックをテスト対象にできる
制限事項
AWS IAM Outbound Identity Federationは、AWSワークロードのIdentityを外部サービスに伝えるための仕組みであり、ユーザー認証や詳細な認可には向いていません。
まとめ
AWS IAM Outbound Identity Federation(GetWebIdentityToken API)は、従来のマイクロサービス認証が抱えていた課題を解決する画期的な機能になりえます。
従来のアプローチとの比較
| 課題 | 従来のアプローチ | Outbound Identity Federation |
|---|---|---|
| リージョン間通信 | Security Groupで不可能、CIDRブロック必要 | JWT で実現可能 |
| アカウント間通信 | VPC Peeringや複雑な設定が必要 | JWT で実現可能 |
| LoadBalancer経由 | カスタムヘッダーなど回避策が必要 | 標準的なAuthorizationヘッダーで実現 |
| シークレット管理 | Secrets Manager等が必要 | IAMロールのみで実現 |
| 追加コスト | Cognito、Secrets Managerなど | 完全無料 |
| 実装複雑性 | サービス固有の実装が必要 | Spring Securityの標準機能で実現 |
本実装の本質的な価値:
- ネットワーク境界を超越: VPCやリージョンといった物理的な境界に制約されない
- 多層防御の実現: API Gatewayに加え、各マイクロサービスでも認証を実施
- 運用負荷の最小化: IAM権限管理だけで完結
従来のベストプラクティスが「多層防御」を推奨しながらも、その実現に多くのリソースとコストを必要としていたのに対し、この実装はシンプルさ、セキュリティ、コスト効率の三つを同時に達成しています。
適切なユースケース(特にワークロード間通信)で使えば、シンプル・セキュア・低コストな認証を実現できる強力な機能です。今後、Lambda・EC2・異なるAWSアカウント間での活用も期待できます。
Discussion