# GrantedでAWSの平文キーをSSOに置き換える — 見分けにくい2つのAWSログインに要注意

> GrantedとAWS IAM Identity Centerは、Windows上の静的IAMキーを短命なSSO認証情報に置き換えます。従来のAWSサインイン画面はこの認証情報を受け付けません。

- Source: https://oharu121.com/ja/blog/granted-windows-plaintext-aws-key-iam-identity-center-sso/
- Published: 2026-08-22T11:19:22+09:00
- Updated: 2026-08-23T06:47:48+09:00
- Tags: AWS, Granted, 開発ツール

---
**要点**

- 漏洩したAWSキーは、新しいキーに置き換える必要はありません。GrantedとAWS IAM Identity Centerを組み合わせれば、静的なIAMシークレットをブラウザ発行の短命な認証情報に置き換えられ、ディスク上に静的な値が一切残りません。
- signin.aws.amazon.comの従来のサインイン画面と、IAM Identity CenterのAWSアクセスポータルは、互いに無関係な2つのログインシステムです。Identity Centerのユーザー名は、パスワードが正しくても従来の画面では絶対に機能しません。
- `assume`はPATHに置くだけでは不十分で、シェルのプロファイルにドットソース実行する必要があります。単なる`.exe`は、自分を起動した親シェルの環境変数を書き換えられないためです。そのままドットソース実行すると新しいターミナルを開くたびにピッカーが再現され、同じ行をPowerShellの関数でラップすれば、実際に`assume`と入力されるまで実行を遅延させられます。
- `AdministratorAccess`は無条件ではありません。バケット自身のリソースベースポリシーに書かれた明示的な`Deny`が、管理者権限のセッションでもバケット削除をブロックしました。
- `[Environment]::SetEnvironmentVariable`によるPATHの変更は、すでに起動しているシェルには反映されません。Windowsで CLI をインストールした直後に出る「コマンドが見つからない」は、たいていこれが原因です。

## はじめに

AWSの設定ファイルにある、本題とは無関係なリージョンの typo を確認していたとき、エージェントがそのファイルをターミナルに出力するコマンドを実行し、すぐ下に並んでいたアクセスキーIDとシークレットのペアも平文のまま、機密扱いされていないセッションに出力してしまいました。そのキーは2025年2月からアカウント唯一の認証情報で、`~/.aws/credentials`に置かれた1つの静的なIAMユーザーシークレットであり、有効期限もローテーションもありませんでした。私はその場でこのキーの使用をやめ、永続的なキーの代わりにAWS IAM Identity Centerの短命なSSO認証情報をラップするCLIであるGrantedへ移行することを決めました。移行自体はうまくいきましたが、途中で正しそうに見えて実は違うログイン画面を通ることになりました。本記事では、WindowsへのGrantedとIAM Identity Centerの構築と、実際に時間を取られた箇所、つまり渡されたログイン情報を絶対に受け付けないサインイン画面について整理します。

## キーが平文で出てきた

このアカウントにはAWS CLIプロファイルが`default`の1つだけあり、Next.jsのデプロイパイプライン用に作られたIAMユーザーの静的アクセスキーで認証していました。本題とは無関係な`region = ap-northeast-1c`というtypo（これはリージョンではなくアベイラビリティーゾーンです）を直すために`~/.aws/config`と`~/.aws/credentials`を読んだ結果、両方のファイルをターミナルに出力することになりました。`~/.aws/credentials`にはアクセスキーIDとシークレットアクセスキーがそのまま入っていました。

**有効期限のない長命なキーは、見えてはいけない場所に一度でも見えた瞬間、常設のリスクになります**。チャットの記録はまさにそういう場所です。ここでの正しい対応は、キーをローテーションしてそのまま使い続けることではありませんでした。マシン上に静的なキーを一切置かないようにすることでした。

## WindowsへのGranted導入

[Granted](https://granted.dev)は、ロールを切り替える`assume`とセットアップ・アカウント管理を行う`granted`という2つのコマンドの裏に、AWS IAM Identity CenterのSSOフローをラップしています。そのおかげで、ターミナルセッションは永続的なシークレットの代わりに短命な認証情報を保持することになります。Windowsは、Granted自身のドキュメントが主に想定しているプラットフォームではなく、Windows版のインストールに関する3点はREADMEを読んだだけではわかりませんでした。

**リリースのアセットはアーキテクチャごとに分かれていて**、選択を誤ってもわかりやすいエラーにはならず、静かに失敗します。先に確認しておけば推測せずに済みました。

```powershell
echo $env:PROCESSOR_ARCHITECTURE
```

結果は`AMD64`で、つまり`arm64`ではなく`windows_x86_64`版が必要ということです。展開したアーカイブには`granted.exe`、`assumego.exe`、そしてシェルごとの`assume.ps1`/`assume.bat`/`assume`が入っています。これらをすべて恒久的なフォルダにコピーし、そのフォルダをユーザーの`PATH`に追加するところまでは簡単でした。

**`assume`は実行ファイルとしてPATHに置くだけでは機能しません。ロールをassumeするとは、呼び出し元シェルの環境変数を書き換えることだからです**。子プロセスは親のためにそれを行うことが絶対にできません。単なる`.exe`は自分自身のプロセスで実行されて終了し、そこで設定したものはすべてそのプロセスと一緒に消えます。代わりに`assume.ps1`は現在のシェルの*内側*で実行される必要があり、それには新しいセッションが起動するたびに自動で読み込まれるよう、PowerShellのプロファイルにドットソース実行しておく必要があります。

```powershell
. "$env:USERPROFILE\granted\assume.ps1"
```

これを`Microsoft.PowerShell_profile.ps1`に置きます。これを省略してPATH経由でそのまま`assume`を実行しても、スクリプト自体は実行されますが、そこでエクスポートされた認証情報は子プロセスが終了した瞬間に消え、親シェルは以前とまったく同じ未認証のままになります。

*Figure — AssumeSourcing: 単なる`.exe`は、終了時に破棄される子プロセスの中に認証情報を設定します。ドットソース実行は子プロセスそのものをなくすため、同じ変数が親シェルに直接入り、そこに留まります。*

**あるシェルで行った`PATH`の変更は、すでに起動しているシェルには反映されません。** Grantedのフォルダを

```powershell
[Environment]::SetEnvironmentVariable("Path", $newPath, "User")
```

でユーザーの`PATH`に追加すると、レジストリには書き込まれますが、それを読むのは起動時の*新しい*プロセスだけです。変更が行われた時点ですでに生きているプロセスには何の効果もありません。すでに開いていたターミナルはどれも

```
granted.exe : The term 'granted.exe' is not recognized as the name of a cmdlet, function, script file, or operable program.
```

というエラーを出し続け、そのシェルがユーザースコープとマシンスコープ両方から`$env:Path`を再読み込みするか、新しいターミナルを開くまで直りませんでした。**この同じPATHの罠は、この移行の中でさらに2回顔を出しました**。1回は同じセッション内で後から再インストールした`pnpm`について、もう1回は別のシェル（Bash）で、同じツールに対してまったく別の、それ自体壊れた`PATH`エントリがあったケースです。

## AWS Organizationなしで IAM Identity Center を構築する

GrantedのSSOフローが通信する先には、実際のIAM Identity Centerインスタンスが必要です。そのため、ローカルのプロファイルを設定する前に、まずアカウント側でIdentity Centerを有効化する必要がありました。AWSのドキュメントには2種類のインスタンスがあり、1つはAWS Organizationsを必要とする組織インスタンス、もう1つはそれを必要とせず単一のスタンドアロンアカウントに限定されるアカウントインスタンスです。

**有効化画面のデフォルトの導線は組織を作成するもので、これにはクリックする前に知っておく価値のある課金上の影響があります。** AWSのドキュメント自体がはっきりと述べている通り、無料利用枠のアカウントで、デフォルトの組織インスタンスを持たせるためにAWS Organizationを作成すると、そのアカウントは即座に従量課金制に切り替わり、残っていた無料利用枠のクレジットは失効します。この画面にはもう一方の選択肢であるアカウントインスタンスも用意されていますが、ボタンではなく副次的なリンクとしてです。何もクリックする前にドキュメントを読んでおいたことで、これは課金上の想定外の事態になる前に気づくことができました。

アカウントインスタンスを有効化すると、一見設定ミスに見えて実はそうではない、もう1つの引っかかりが出てきました。コンソールに**「IAM Identity Center is currently configured in the US East (N. Virginia) Region」**と表示され、これは自分が把握している範囲では何もそこに設定していないにもかかわらず表示されたのです。このバナーは、そのアカウントに対してすでに該当リージョンにインスタンスが存在するときにしか出ません。そしてアカウントインスタンスは作成されたリージョンに永久に固定され、後からリージョンを変えるにはインスタンスを削除し、すべてのユーザー・権限セット・割り当てをゼロから作り直す必要があります。このリージョン固定を、アカウントの実リソースがどこで動くかまで制約するものと読んでしまいがちですが、そうではありません。**Identity Centerのリージョンが決めるのは、自身のIDメタデータがどこに置かれるかだけです**。EC2インスタンスやS3バケットなど、アカウント内の他のものがどこに置けるかには関係ありません。ログインの仕組みをバージニアから管理しながら、実際のリソースはすべて東京に置いておくのはごく普通のことで、言及に値するようなレイテンシーのコストもありません。

## 1つに見える2つのログイン

アカウントインスタンスが立ち上がった後は、想定通りにセットアップが進みました。ユーザーを1つ作り、権限セット（`AdministratorAccess`）を1つ作り、その2つをアカウントに紐づける割り当てを作りました。うまくいかなかったのはそれでログインするところで、しかも役に立つエラーが一切出ないという形で失敗しました。

AWSにサインインする場所として真っ先に思いつくのは、AWSのチュートリアルやブックマークがどれも指す`signin.aws.amazon.com`です。ここにアカウントID、新しく作ったIdentity Centerのユーザー名、そのパスワードを入力しても、理由のヒントも一切ないまま毎回ログインを拒否されました。**従来のサインイン画面とIdentity Centerのログインは、互いに何も共有しない別々のシステムです**。ユーザーディレクトリも、パスワードストアも、一致するアカウントという概念すら共有していません。従来の画面が認証するのは、IAM → ユーザーで作成されたIAMユーザーであり、このアカウントが持つ唯一のIAMユーザーは元のデプロイ用ユーザーである`nextjs-deploy`だけで、Identity Centerのユーザーがそこに登録されることは決してありません。Identity Centerのアイデンティティのパスワードは、正しいかどうかに関わらず、あの画面を満たすことは絶対にありません。

*Figure — TwoLogins: 2枚のカードをつなぐ線をあえて描いていません。従来のサインイン画面とAWSアクセスポータルは異なる2つのユーザーディレクトリを認証するため、片方で通用するものはもう片方ではまったく通用しません。*

実際に機能する入り口は、アカウント自身のSSO開始URL（`https://d-xxxxxxxxxx.awsapps.com/start`、Identity Centerコンソールの Settings ページで確認できます）からたどり着くAWSアクセスポータルの方でした。ここでログインすると、アカウントとそれに割り当てられた権限セット`AdministratorAccess`がクリック可能なリンクとして並ぶページが表示されます。**その横にある小さな鍵アイコンではなく、権限セットの名前自体をクリックする**ことで、従来のサインインフローと同じくフルのAWS Management Consoleが開きます。鍵アイコンの方はCLI用の認証情報を表示するだけです。開くのは永続的なIAMログインの代わりに、一時的なフェデレーテッドセッションを伴うコンソールです。

## メールアドレスの重複と、ユーザーの作り直し

最初に作ったIdentity Centerユーザーは、サブで使っているメールアドレスで作成していて、前述のログイン問題を一度も突破できませんでした。原因はログイン画面の方であって認証情報ではないと理解した後、パスワードをリセットして再挑戦しましたが、それでも認証に失敗しました。**一度も成功したことのないログインのデバッグを続けるより、ユーザーを削除して作り直す方が単純に見えた**ため、実際に日常使っているメールアドレスで作り直すことにしました。

1回目の試行では単純にはいきませんでした。

```
Errors occurred while adding user "oharu121-dev"
The user couldn't be added
Duplicate unique attribute value for emails.value
```

**Identity Centerはディレクトリ全体で1つのメールアドレスにつき1ユーザーというルールを強制しています**。そして、最初の壊れたままのユーザーがまだそのアドレスを持っていました。先にそちらを削除し、同じメールアドレスで`oharu121-dev`を作り直すと、今度は問題なく通りました。今回は従来の画面ではなくAWSアクセスポータル経由でログインしたところ、1回目の試行で成功し、それまでの見立てが裏付けられました。**このアカウントには、機能しないパスワードがあったのではなく、間違ったシステムに対してログインを試みていただけでした**。

## Grantedを新しいログインにつなぐ

Identity Centerのユーザーが機能するようになったので、ローカルのAWS CLIプロファイルを生成するのはコマンド1つで済みました。

```powershell
granted sso populate --sso-region us-east-1 https://d-xxxxxxxxxx.awsapps.com/start
```

これによりブラウザが開いて1回限りのデバイス認可が行われ、静的なキーの代わりにGranted自身の認証情報リゾルバーを使うプロファイルが書き込まれました。

```ini title=".aws/config"
[profile oharu121/AdministratorAccess]
granted_sso_start_url      = https://d-xxxxxxxxxx.awsapps.com/start
granted_sso_region         = us-east-1
granted_sso_account_id     = 123456789012
granted_sso_role_name      = AdministratorAccess
credential_process         = granted credential-process --profile oharu121/AdministratorAccess
```

実際のターミナルウィンドウで`assume oharu121/AdministratorAccess`を実行し、動作を確認できました。

```json
{
    "UserId": "AROAR6BADHEROZFPENFQ5:oharu121-dev",
    "Account": "123456789012",
    "Arn": "arn:aws:sts::123456789012:assumed-role/AWSReservedSSO_AdministratorAccess_.../oharu121-dev"
}
```

以前の`arn:aws:iam::...:user/nextjs-deploy`の代わりに、`assumed-role`のARNになっています。

**`assume`は、プロファイル名を引数に正確に指定しても、非対話型のシェルからは動かせません。** 何を選んでも矢印キーのピッカーが確認のために開き、そのピッカーはスクリプトやツール駆動のセッションから読み取れるコンソール入力を持たないためです。

```
? Please select the profile you would like to assume:  [Use arrows to move, type to filter]
[✘] Incorrect function.
```

そのため、この移行の中の`assume`呼び出しはすべて、実際に人が張り付いているターミナルで実行する必要がありました。**`credential_process`は、裏側のSSOトークンがキャッシュされていれば、設計上は非対話式です**。そのため、素の`aws`コマンドは、人が張り付いていようがいまいがどこでも動きます。`[default]`プロファイルを同じリゾルバーに向けることで、コマンドごとに`--profile`を入力する必要がなくなりました。

```ini title=".aws/config"
[default]
region = ap-northeast-1
output = json
credential_process = granted credential-process --profile oharu121/AdministratorAccess
```

## 新しいターミナルを開くたびにピッカーが再現した

何かを入力する前に、ただPowerShellのウィンドウを開くだけで、毎回そのままインタラクティブなピッカーに落ちるようになりました。

```
? Please select the profile you would like to assume:  [Use arrows to move, type to filter]

> oharu121/AdministratorAccess
  default
[✘] interrupt
```

**原因は、本記事が先に勧めていたそのプロファイル行でした。** エージェントが`Microsoft.PowerShell_profile.ps1`を読み直したところ、上で紹介したのと同じ行が見つかりました。

```powershell
. "$env:USERPROFILE\granted\assume.ps1"
```

`assume.ps1`は自身のロジックを関数でラップしていません。ドットソース実行しているその行で、直接`assumego`を実行します。

```powershell
$ASSUME_FLAG, $ASSUME_1, ... = `
$(& (Join-Path $PSScriptRoot -ChildPath "assumego") $args) -split '\s+'
```

そして`assumego --help`が示す使い方は`assume [options][Profile]`で、プロファイル引数がなければインタラクティブなピッカーにフォールバックします。まさに空の`$args`が生む状態です。**スクリプトをそのままプロファイルにドットソース実行すると、その行はシェル起動のたびに即座に実行され、渡すプロファイル引数もないため、ピッカーは最初の1回だけでなく毎回のターミナルで発火していました。**

修正はドットソース実行そのものは変えず（単なる`.exe`は依然として親シェルの環境変数を書き換えられないため）、代わりにそれを関数の後ろに遅延させ、実際に`assume`と入力されたときだけその行が実行されるようにするというものです。

```powershell
function assume { . "$env:USERPROFILE\granted\assume.ps1" @args }
```

**この関数を定義するだけのプロファイルは、起動時には何も実行しません。** その後`assume oharu121/AdministratorAccess`を実行すると、以前とまったく同じように動作しました。`@args`によるスプラットが、シェル起動のたびにではなく必要になった時点でプロファイル名を`assumego`まで届けます。

## 動作確認と、管理者権限で見えたもの

`--profile`フラグなしで`aws sts get-caller-identity`を実行し、`default`プロファイルがSSO経由で解決されるようになったことを確認しました。次のステップは、静的なキーを完全に退役させることでした。まず`.github/workflows`やAWS SDK、キーID自体が出てきそうな場所を含めて、このブログのリポジトリのCI設定やソースコードにそのキーへの参照がないか確認したうえで、`nextjs-deploy`ユーザーに対して`aws iam delete-access-key`を実行しました。**リポジトリ内にはそのキーへの参照は一切ありませんでした**。つまり、このブログはホスティング先自身のGit連携を通じてデプロイされており、そのキー経由ではなかったということです。

この管理者セッションのおかげで、アカウント内に他にまだ動いているものがないか監査することもできました。**稼働中のコンピュートリソースやアクティブなElastic Beanstalk環境は残っていませんでした**。残っていたのは、数キロバイトしかない、とっくに放棄されたプロジェクトのデプロイ成果物である孤立したS3バケットが2つと、何もデプロイされていない空のElastic Beanstalkの「Application」レコードが2つだけでした。

バケットを削除しようとしたところ、`AdministratorAccess`が実は管理者権限そのものではなかった唯一の箇所が明らかになりました。

```
An error occurred (AccessDenied) when calling the DeleteBucket operation:
User: arn:aws:sts::123456789012:assumed-role/AWSReservedSSO_AdministratorAccess_.../oharu121-dev
is not authorized to perform: s3:DeleteBucket on resource: "arn:aws:s3:::elasticbeanstalk-..."
with an explicit deny in a resource-based policy
```

このバケットはElastic Beanstalkが自動作成したもので、AWSがデフォルトで付与するステートメントを含む独自のリソースベースポリシーを持っていました。

```json
{
  "Sid": "eb-58950a8c-feb6-11e2-89e0-0800277d041b",
  "Effect": "Deny",
  "Principal": { "AWS": "*" },
  "Action": "s3:DeleteBucket",
  "Resource": "arn:aws:s3:::elasticbeanstalk-..."
}
```

**明示的な`Deny`は、AWSのポリシー評価においてすべての`Allow`に優先します。ワイルドカードな`AdministratorAccess`権限セットも例外ではありません**。これは特定のアイデンティティに向けて書かれたものではなく、誰に対しても書かれているため、管理者権限であっても上回ることができないからです。まずバケット自身のリソースベースポリシーを削除し、その後バケットを削除することで解消しました。`AdministratorAccess`が意味するのは、*IDベース*のポリシーが邪魔をしないということだけであり、リソース自身のポリシーがそのリソース自身の邪魔をすることについては何も言っていません。

## まとめ

**このアカウントは最終的に静的なAWSキーがゼロになりました**。古いIAMユーザーのアクセスキーはIAMから削除され、`~/.aws/credentials`は空になり、すべての`aws`コマンドはデフォルトでGrantedの`credential_process`を経由してIAM Identity CenterのSSOに解決されるようになりました。ここに至るまでには、何かをインストールする前のアーキテクチャ確認、単なるPATHエントリでは代替できないPowerShellプロファイルの編集、望まないAWS Organizationを避けるためのアカウントインスタンスの選択、そして間違ったパスワードとは何の関係もないログインの失敗がありました。従来のAWSサインイン画面とIdentity Centerのアクセスポータルは、同じ家に入る2つのドアのように見えます。**しかし実際には2つの別々の家であり、Identity Centerのユーザーのことを知っているのはそのうち片方だけです。**

## 参考リンク

- [Granted: クラウドに最も簡単にアクセスする方法](https://granted.dev)
- [IAM Identity Centerの組織インスタンスとアカウントインスタンス、スタンドアロンアカウントの無料利用枠に関する課金の注記を含む](https://docs.aws.amazon.com/singlesignon/latest/userguide/organization-instances-identity-center.html)
- [IAM Identity CenterでAWSリージョンを切り替える、既存のインスタンスが削除なしに移動できない理由も含めて](https://docs.aws.amazon.com/singlesignon/latest/userguide/switching-regions.html)
