yamory_techblog_logo
techblog_azure_to_aws

Azure→AWSのシークレットレス連携

藤田優貴
2025.6.30

はじめに

yamoryプロダクト開発グループの藤田です。

先日yamoryではクラウドアセットスキャン機能をMicrosoft Azure向けにリリースしました。この記事では、その裏側で使われている技術を紹介します。

yamoryではメインのインフラとしてAWSを利用しています。AWS向けのクラウドアセットスキャンではクロスアカウントのAssume Roleを利用することで、シークレットを設定しなくてもお客様環境における権限をいただいて、セキュアにスキャンを実行できます。しかし、Azureについては同じ技術を利用することができません。

そこでこの記事では、まず前半としてAzureからAWS環境に向けてスキャン結果を送信する際に、シークレットを使わずにアクセスする方法について説明していきます。なお、後日後半としてAWSからAzureに対してスキャンリクエストを送信する際に、シークレットを利用しない方法について説明予定です。

AWS Marketplaceについて

まず、AWS Marketplaceとは何かについて簡単にご説明します。AWS Marketplaceは、AWSを利用しているユーザーなら誰でも利用できる、サードパーティのソフトウェアやサービスを容易に検索、購入、デプロイできるオンラインストアです。

yamoryにとって、AWS Marketplaceの導入は、商流の拡大という大きなメリットをもたらします。AWSを利用している多くの企業様に対して、yamoryをより直接的に提供できるようになります。また、AWSの請求システムを通じて契約や決済が効率化されることで、ユーザー側の運用負荷の軽減にも繋がります。

技術概要

Azure Container Appで動いているアプリケーションから、AWS S3にファイルをPUTするシステムを想定します。

azure_to_aws

通常であれば、AWSから発行したIAM Userのシークレットを用いて認証を行いますが、今回はシークレットを利用せずに、Azure上のContainer Appに紐付けてあるManaged IdentityからAWSのIAM Roleに対してAssume Roleを行って認証情報を得ます。シークレットを使う際には、次のようなデメリットが存在するためです。

  • 漏洩リスクを低減するためシークレットを安全に保管する方法を考慮する必要あり
    • Azure Key Vaultのような追加サービスを利用する必要が発生
  • 漏洩した場合、持っている権限の全てを利用可能
    • 該当のS3にPUTを行う権限しか割り当てていないが、ファイルを改ざん可能
    • IAM Userのシークレットはデフォルトで無期限のため、漏洩に気づけないと無期限で利用される
  • 適切な間隔でシークレットのローテーション作業が必要
    • 一定時間で有効期限が切れるようにしてあると、ローテーション作業が行われなかった際にサービス停止のおそれ
    • そもそもローテーションの作業が運用時の手間

ここでAssume Roleを利用すると、次のようなメリットが得られます。

  • そもそもシークレットを保存する必要が無い
  • ローテーション作業の失念によるサービス停止リスクを負わなくて良い
    • 接続の短期的なシークレットが発行されるため
  • 長期間認証状態が保持されるリスクを回避可能
    • Assume Roleのセッション保持時間はデフォルトで1時間となるため
  • IAM Roleの接続元を該当のManaged Identityに限定可能
    • Managed Identityを紐付けてあるContainer AppからしかAssume Roleができないため、よりセキュア

こうしたメリットを踏まえ、IAM Userのシークレットを使うよりも手順が複雑となりますが、実装方法を説明していきます。

実装の詳細

実装には、Azure側とAWS側、双方の準備が必要です。順を追って説明していきます。また、サンプルコードは全てGo言語で書かれていますが、他の言語向けSDKを使っても同じAPIを呼び出せば同様の実装ができるはずです。インフラ定義には、Azure側はBicepを、AWS側はCDK(TypeScript)を利用します。

Azure側の準備

Azure側にはContainer Appと、それに紐付けるUser Assigned Managed Identityが必要です。また、Assume Roleを受け入れるためEntra ID Applicationの登録も必要です。なお設定に必要な値だけ抜粋して掲載していますので、その他動作に必要なプロパティについては別途指定が必要です。

resource assumeRoleManagedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = {
  name: 'assume-role-identity'
}

var uniqueName = 'assume-role-entra-id-app'

resource assumeRoleApp 'Microsoft.Graph/applications@v1.0' = {
  displayName: 'assume-role-entraid-app'
  uniqueName: uniqueName
  // AWS で assume role を受け入れるための identifier url を生成
  identifierUris: [
    'api://${uniqueName}'
  ]
  appRoles: [
    // 権限を受け渡せるようにアプリロールを作成
    {
      id: guid(uniqueName)
      allowedMemberTypes: ['User', 'Application']
      description: 'This app role will use AWS STS AssumeRoleWithWebIdentity API to obtain AWS temporary credentials.'
      displayName: 'AssumeRoleAppRole'
      isEnabled: true
      value: 'AssumeRoleWithWebIdentity'
    }
  ]
}

resource servicePrincipal 'Microsoft.Graph/servicePrincipals@v1.0' = {
  appId: assumeRoleApp.appId
}

// サービスプリンシパルとアプリロールの紐付け
resource appRoleAssignment 'Microsoft.Graph/appRoleAssignedTo@v1.0' = {
  appRoleId: assumeRoleApp.appRoles[0].id
  principalId: assumeRoleManagedIdentity.properties.principalId
  resourceId: servicePrincipal.id
}

resource containerApp 'Microsoft.App/jobs@2024-02-02-preview' = {
  name: 'containerApp'
  // managed identity の紐付け
  identity: {
    type: 'UserAssigned'
    userAssignedIdentities: {
      '${assumeRoleManagedIdentity.id}': {}
    }
  }
  properties: {
    template: {
      containers: [
        {
          // Assume Role に必要な情報を環境変数経由で受け渡す
          env: [
            {
              name: 'AZURE_APP_ID_URI'
              value: 'api://${uniqueName}'
            }
            {
              name: 'AZURE_MANAGED_ID_CLIENT_ID'
              value: assumeRoleManagedIdentity.properties.clientId
            }
          ]
        }
      ]
    }
  }
}

AWS側の準備

AWS側には権限を受け渡すIAM Roleと、OpenIdConnectProviderの作成が必要です。ここで作成したロールをAzure側から利用します。

// 接続先 Azure EntraID のテナントID
const azureTenantId = 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx';
// Bicep で定義した UniqueName と同一の値を利用
const azureAppUniqueName = 'assume-role-entra-id-app';
const azureAppIdUrl = 'api://${azureAppUniqueName}';
// 上記で作成した assumeRoleManagedIdentity の ObjectId
const azureManagedIdObjectId = 'yyyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy';

const provider = new OpenIdConnectProvider(this, `${projectName}-azure-assume-role-provider`, {
  url: `https://sts.windows.net/${azureTenantId}/`,
  clientIds: [azureAppIdUrl],
});

const condition: Condition = {};
const audience = `sts.windows.net/${azureTenantId}/:aud`;
const subject = `sts.windows.net/${azureTenantId}/:sub`;
condition[audience] = azureAppIdUrl;
condition[subject] = azureManagedIdObjectId;

const role = new Role(this, `${projectName}-azure-assume-role`, {
  roleName: `${projectName}-azure-role-to-assume`,
  assumedBy: new OpenIdConnectPrincipal(provider, {
    StringEquals: condition,
  })
});
role.applyRemovalPolicy(RemovalPolicy.DESTROY);

Azure Container Appの作成

Azure Container Appにデプロイするプログラムを作っていきます。まずはAzure Container Appに紐付けてあるManaged IdentityからAWS OpenIdConnectProvider(以下OIDC Provider)に接続するためのトークンを取得する部分を作っていきます。

Managed Identityを持ったContainer Appは、環境変数を使ってManaged Identityに関する情報を引き出すREST APIエンドポイントを取得できます(1)。このエンドポイントに対し、OIDC Providerとの接続に必要な認証情報を問い合わせます。audienceにはIAM Roleを作成するときに指定したazureAppIdUrlを、client_idには環境変数に格納したAZURE_MANAGED_ID_CLIENT_IDを渡します(2)。この状態でリクエストを送信すると、AzureAccessTokenを得ることができます(3)。

func FetchAzureAccessToken(_ context.Context, audience string, managedIdClientId string) (*AzureAccessToken, error) {
    // (1)
    endpoint := os.Getenv("IDENTITY_ENDPOINT")
    header := os.Getenv("IDENTITY_HEADER")

    url, err := netUrl.Parse(endpoint)
    if err != nil {
       return nil, fmt.Errorf("failed to parse endpoint: %w", err)
    }

    // (2)
    query := url.Query()
    query.Set("api-version", "2019-08-01")
    query.Set("resource", audience)
    query.Set("client_id", managedIdClientId)
    url.RawQuery = query.Encode()

    request, err := http.NewRequest("GET", url.String(), nil)
    if err != nil {
       return nil, fmt.Errorf("failed to create request: %w", err)
    }
    request.Header.Set("X-IDENTITY-HEADER", header)

    client := new(http.Client)
    response, err := client.Do(request)
    if err != nil {
       return nil, fmt.Errorf("failed to get access token: %w", err)
    }

    body, err := io.ReadAll(response.Body)
    if err != nil {
       return nil, fmt.Errorf("failed to read response body: %w", err)
    }

    // (3)
    var azureAccessToken AzureAccessToken
    err = json.Unmarshal(body, &azureAccessToken)
    if err != nil {
       return nil, fmt.Errorf("failed to unmarshal response body: %w", err)
    }

    return &azureAccessToken, nil
}

ここで得たAzureAccessTokenを利用して、AWS STSを使ったassume roleを実行します。 WebIdentityTokenContentGetIdentityToken() ([]byte, error)を実装している必要があるので、Azureから取得したトークンをブリッジできるように定義を先に書いておきます。

type WebIdentityTokenContent string

var _ stscreds.IdentityTokenRetriever = WebIdentityTokenContent("")

func (f WebIdentityTokenContent) GetIdentityToken() ([]byte, error) {
    return []byte(f), nil
}

先に通常通りstsClientを生成したら、NewWebIdentityRoleProviderメソッドを使ってAssume Roleを行います。Assume Role先のロール名、先ほど取得したトークン、オプションの設定を書き込んで呼び出すとAssume Roleが行われ、アクセストークンが得られます。
ここでAWS Go SDK限定ではありますが、認証キャッシュを持つことができます。長時間作業を行うような場合に、認証情報を自動的にリフレッシュしてくれます。NewCredentialsCacheを呼び出すことで設定できるので、使っておくと便利です(2)。

func ConfigureAWSWithWebIdentity(_ context.Context, webIdentityToken WebIdentityTokenContent, roleArn string) (aws.Config, error) {
    var cfg aws.Config
    var err error
    cfg, err = config.LoadDefaultConfig(context.Background(), config.WithRegion("ap-northeast-1"))
    if err != nil {
       return cfg, fmt.Errorf("failed to loading default config: %w", err)
    }

    stsClient := sts.NewFromConfig(cfg)

    // (1)
    provider := stscreds.NewWebIdentityRoleProvider(stsClient, roleArn, webIdentityToken, func(o *stscreds.WebIdentityRoleOptions) {
       o.RoleSessionName = "azure-assume-role-session"
       o.Duration = 1 * time.Hour
    })

    // (2)
    cfg.Credentials = aws.NewCredentialsCache(provider, func(options *aws.CredentialsCacheOptions) {
       options.ExpiryWindow = 5 * time.Minute
       options.ExpiryWindowJitterFrac = 0.5
    })

    if _, err := cfg.Credentials.Retrieve(context.Background()); err != nil {
       return cfg, fmt.Errorf("error retrieving credentials: %w", err)
    }
    return cfg, nil
}

あとはここで返したaws.Configの値を各種クライアントに引き渡せば、シークレットなしでAssume Roleを実施した状態で安全にAWS上のリソースがAzureから扱うことができます。

おわりに

ここまで、具体的なインフラ定義とアプリケーションコードを示しながら、Azureからシークレットを使わずかつセキュアにAWS上のリソースを利用する方法をご紹介しました。シークレットを用いる方法は簡便ではありますが、漏洩時などの手間を考えると、最初から持たないことに越したことはありません。これを機に、皆さんの実装もシークレットレスに切り替えてみてはいかがでしょうか。