<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>AssumeRole on J2eff's Garage</title><link>https://blog.j2eff.me/tags/assumerole/</link><description>Recent content in AssumeRole on J2eff's Garage</description><generator>Hugo -- gohugo.io</generator><language>ko-KR</language><lastBuildDate>Mon, 06 Jul 2026 13:00:00 +0900</lastBuildDate><atom:link href="https://blog.j2eff.me/tags/assumerole/index.xml" rel="self" type="application/rss+xml"/><item><title>루트키를 넘기지 마라 — 역할과 AssumeRole</title><link>https://blog.j2eff.me/p/%EB%A3%A8%ED%8A%B8%ED%82%A4%EB%A5%BC-%EB%84%98%EA%B8%B0%EC%A7%80-%EB%A7%88%EB%9D%BC-%EC%97%AD%ED%95%A0%EA%B3%BC-assumerole/</link><pubDate>Mon, 06 Jul 2026 13:00:00 +0900</pubDate><guid>https://blog.j2eff.me/p/%EB%A3%A8%ED%8A%B8%ED%82%A4%EB%A5%BC-%EB%84%98%EA%B8%B0%EC%A7%80-%EB%A7%88%EB%9D%BC-%EC%97%AD%ED%95%A0%EA%B3%BC-assumerole/</guid><description>&lt;!--
제목 후보 (Jeff 선택용):
 1. 루트키를 넘기지 마라 — 역할과 AssumeRole ← 현재 가안
 2. 모자를 빌리다
 3. 역할: 만들고, 빌리기
--&gt;
&lt;p&gt;8편에서 &lt;code&gt;CreateAccount&lt;/code&gt;는 새 계정의 &lt;strong&gt;루트 자격증명을 통째로 돌려줬다.&lt;/strong&gt; 편했지만, 그러면서 정직하게 인정한 게 있다 — 이건 AWS가 실제로 하는 안전한 방식이 아니라고. 장기 루트키는 유출되면 영원히, 전권으로 뚫린다. 이번 편은 그 빚을 갚는다. 루트키를 넘기는 대신, &lt;strong&gt;역할을 빌려 임시 자격증명&lt;/strong&gt;으로 들어간다.&lt;/p&gt;
&lt;h2 id="역할은-신분이-아니라-빌리는-모자"&gt;역할은 신분이 아니라 빌리는 모자
&lt;/h2&gt;&lt;p&gt;지금까지 mincloud의 모든 신원은 영구 자격증명을 가졌다. 루트도, IAM 유저도 각자 키+시크릿이 있고, 안 지우면 영원히 유효했다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;역할(Role)은 다르다. 자기 자격증명이 없다.&lt;/strong&gt; 역할은 잠깐 쓰고 벗는 모자다. 두 가지를 가진다 — 이 모자를 쓰면 뭘 할 수 있나(권한), 그리고 &lt;strong&gt;누가 이 모자를 써도 되나(신뢰 정책)&lt;/strong&gt;. 아무도 역할로 로그인하지 않는다. 자기 자신으로 시작해서, 역할을 &lt;strong&gt;빌려서(assume)&lt;/strong&gt; 잠깐 그것이 된다.&lt;/p&gt;
&lt;p&gt;그러니 순서가 정해진다. &lt;strong&gt;역할이 있어야 빌릴 수 있다.&lt;/strong&gt; &lt;code&gt;CreateRole&lt;/code&gt;이 &lt;code&gt;AssumeRole&lt;/code&gt;보다 먼저다.&lt;/p&gt;
&lt;h2 id="먼저-역할을-만든다"&gt;먼저: 역할을 만든다
&lt;/h2&gt;&lt;p&gt;멤버 계정을 하나 만들고(8편의 그 문), 그 계정 안에 역할을 만든다. 핵심은 &lt;code&gt;--assume-role-policy-document&lt;/code&gt; — &lt;strong&gt;신뢰 정책&lt;/strong&gt;이다.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;$ aws iam create-role --role-name OrgAccess \
 --assume-role-policy-document &amp;#39;{
 &amp;#34;Version&amp;#34;: &amp;#34;2012-10-17&amp;#34;,
 &amp;#34;Statement&amp;#34;: [{
 &amp;#34;Effect&amp;#34;: &amp;#34;Allow&amp;#34;,
 &amp;#34;Principal&amp;#34;: { &amp;#34;AWS&amp;#34;: &amp;#34;arn:aws:iam::000000000001:root&amp;#34; },
 &amp;#34;Action&amp;#34;: &amp;#34;sts:AssumeRole&amp;#34;
 }]
 }&amp;#39; --endpoint-url http://localhost:9910

arn:aws:iam::207346699092:role/OrgAccess
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;이 JSON이 이 시리즈에서 처음 등장하는 &lt;strong&gt;진짜 정책 문서&lt;/strong&gt;다. 8편의 인가는 &lt;code&gt;isManagementRoot&lt;/code&gt;라는 하드코딩 한 줄이었다. 여기선 &amp;ldquo;누가 이 역할을 빌려도 되나&amp;quot;를 &lt;strong&gt;데이터로&lt;/strong&gt; 적는다 — &amp;ldquo;관리 계정(&lt;code&gt;000000000001&lt;/code&gt;)의 루트만.&amp;rdquo; 서버는 나중에 이 문서를 읽어 판단한다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;TrustAllows&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;trustJSON&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;callerARN&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;callerAccount&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// 문서를 파싱해, Effect=Allow / Action=sts:AssumeRole 이고&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// Principal 이 호출자를 포함하는 Statement 가 있으면 허용.&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;하드코딩 게이트가 처음으로 &lt;strong&gt;읽어서 평가하는 정책&lt;/strong&gt;이 된 순간이다.&lt;/p&gt;
&lt;h2 id="다음-역할을-빌린다"&gt;다음: 역할을 빌린다
&lt;/h2&gt;&lt;p&gt;이제 관리 루트(마스터)로 그 역할을 빌린다.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;$ aws sts assume-role \
 --role-arn arn:aws:iam::207346699092:role/OrgAccess \
 --role-session-name jeff-session \
 --endpoint-url http://localhost:9900
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;돌아오는 건 영구키가 아니다. &lt;strong&gt;임시 자격증명&lt;/strong&gt;이다.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;AccessKeyId: ASIADARWTRNKAN3U4M7T ← ASIA (임시) 프리픽스
Expiration: 2026-07-06T16:36:01+00:00 ← 1시간 뒤 죽음
AssumedRole: arn:aws:sts::207346699092:assumed-role/OrgAccess/jeff-session
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;AKIA&lt;/code&gt;가 아니라 &lt;code&gt;ASIA&lt;/code&gt;로 시작한다 — STS가 발급한 임시키의 표식이다. 그리고 세 조각이 더 있다: &lt;code&gt;SecretAccessKey&lt;/code&gt;, &lt;strong&gt;&lt;code&gt;SessionToken&lt;/code&gt;&lt;/strong&gt;, 그리고 &lt;strong&gt;만료 시각&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;이 임시키로 신원을 물으면, 우리는 &lt;strong&gt;멤버 계정 안에 들어가 있다.&lt;/strong&gt;&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;$ AWS_ACCESS_KEY_ID=ASIA... AWS_SECRET_ACCESS_KEY=... AWS_SESSION_TOKEN=... \
 aws sts get-caller-identity --endpoint-url http://localhost:9900
{
 &amp;#34;UserId&amp;#34;: &amp;#34;AROAVX9IJ90X1PM0DI57:jeff-session&amp;#34;,
 &amp;#34;Account&amp;#34;: &amp;#34;207346699092&amp;#34;,
 &amp;#34;Arn&amp;#34;: &amp;#34;arn:aws:sts::207346699092:assumed-role/OrgAccess/jeff-session&amp;#34;
}
&lt;/code&gt;&lt;/pre&gt;&lt;!-- screenshot: 임시키로 assumed-role 신원이 나오는 터미널 --&gt;
&lt;p&gt;계정 &lt;code&gt;207346699092&lt;/code&gt;. &lt;strong&gt;관리 루트는 이 멤버의 루트키를 한 번도 손에 쥐지 않았다.&lt;/strong&gt; 자기 키로 역할을 잠깐 빌렸을 뿐이고, 1시간 뒤엔 이 임시키도 죽는다. 유출돼도 피해 반경이 &amp;ldquo;영원&amp;quot;이 아니라 &amp;ldquo;한 시간&amp;quot;이다. 이게 8편이 못했던, AWS의 안전한 방식이다.&lt;/p&gt;
&lt;h2 id="session-token과-만료는-서버가-지킨다"&gt;session token과 만료는 서버가 지킨다
&lt;/h2&gt;&lt;p&gt;임시키는 그냥 키+시크릿이 아니다. 요청마다 &lt;code&gt;SessionToken&lt;/code&gt;(&lt;code&gt;X-Amz-Security-Token&lt;/code&gt; 헤더)을 같이 실어야 하고, 만료 전이어야 한다. &lt;code&gt;Authenticate&lt;/code&gt;가 서명을 검증한 뒤 이 둘을 더 본다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;span class="lnt"&gt;7
&lt;/span&gt;&lt;span class="lnt"&gt;8
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;cred&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SessionToken&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;!=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;&amp;#34;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// 임시 자격증명&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Header&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;X-Amz-Security-Token&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;!=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;cred&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SessionToken&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nx"&gt;AuthError&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;Code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;InvalidClientTokenId&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;cred&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Expires&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;IsZero&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Now&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;After&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cred&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Expires&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nx"&gt;AuthError&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;Code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;ExpiredToken&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;영구키엔 이 둘이 비어 있어 그냥 지나간다. 임시키만 이 관문을 더 통과한다.&lt;/p&gt;
&lt;h2 id="신뢰하지-않으면-거부한다"&gt;신뢰하지 않으면 거부한다
&lt;/h2&gt;&lt;p&gt;신뢰 정책은 관리 계정만 적어 뒀다. 그럼 멤버 계정의 &lt;strong&gt;루트 자신&lt;/strong&gt;이 이 역할을 빌리려 하면?&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;$ ...(멤버 루트 키로)... aws sts assume-role \
 --role-arn arn:aws:iam::207346699092:role/OrgAccess --role-session-name intruder

An error occurred (AccessDenied) when calling the AssumeRole
operation: User: arn:aws:iam::207346699092:root is not authorized
to perform: sts:AssumeRole on resource: .../OrgAccess
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;strong&gt;AccessDenied.&lt;/strong&gt; 8편에선 이 판단이 하드코딩 한 줄이었지만, 지금은 &lt;strong&gt;역할에 붙은 JSON 문서&lt;/strong&gt;가 내린 결정이다. 신뢰 정책에 없으면 못 빌린다.&lt;/p&gt;
&lt;h2 id="루프가-닫혔다"&gt;루프가 닫혔다
&lt;/h2&gt;&lt;p&gt;8편에서 멤버 계정을 &lt;em&gt;만들었고&lt;/em&gt;, 9편에서 그 계정에 &lt;strong&gt;안전하게 들어가는 법&lt;/strong&gt;을 얻었다. 진짜 AWS의 &lt;code&gt;OrganizationAccountAccessRole&lt;/code&gt;이 바로 이거다 — 계정 만들 때 관리 계정을 신뢰하는 역할을 하나 심어두고, 관리 계정이 그걸 assume해서 들어간다. 우리가 손으로 한 &lt;code&gt;create-role&lt;/code&gt;(신뢰=관리 계정) + &lt;code&gt;assume-role&lt;/code&gt;이 정확히 그 그림이다. 마법이 아니라, 원시 도구 두 개의 조합.&lt;/p&gt;
&lt;p&gt;정직하게 남는 것도 있다. 지금 &lt;code&gt;CreateRole&lt;/code&gt;엔 &lt;strong&gt;권한 검사가 없다&lt;/strong&gt; — 인증만 되면 누구나 자기 계정에 역할을 만든다. 그리고 역할은 신뢰 정책(누가 빌리나)은 있지만, &lt;strong&gt;아직 권한 정책(빌린 뒤 뭘 할 수 있나)은 없다.&lt;/strong&gt; 임시키는 지금 사실상 그 계정의 전권을 갖는다.&lt;/p&gt;
&lt;p&gt;그 두 구멍이 다음 편을 가리킨다. 오늘 trust policy로 &amp;ldquo;누가 빌리나&amp;quot;를 데이터로 판단하기 시작했으니, 같은 엔진을 &lt;strong&gt;&amp;ldquo;누가, 무슨 행동을, 무슨 리소스에&amp;rdquo;&lt;/strong&gt; 로 일반화하면 — 그게 IAM 정책, 진짜 인가 엔진이다. 하드코딩 한 줄에서 시작해 신뢰 정책까지 온 이 흐름의 종착지다.&lt;/p&gt;</description></item></channel></rss>