<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>부트스트랩 on J2eff's Garage</title><link>https://blog.j2eff.me/tags/%EB%B6%80%ED%8A%B8%EC%8A%A4%ED%8A%B8%EB%9E%A9/</link><description>Recent content in 부트스트랩 on J2eff's Garage</description><generator>Hugo -- gohugo.io</generator><language>ko-KR</language><lastBuildDate>Mon, 06 Jul 2026 11:00:00 +0900</lastBuildDate><atom:link href="https://blog.j2eff.me/tags/%EB%B6%80%ED%8A%B8%EC%8A%A4%ED%8A%B8%EB%9E%A9/index.xml" rel="self" type="application/rss+xml"/><item><title>맨 아래 거북이 — 제네시스 계정과 신뢰의 뿌리</title><link>https://blog.j2eff.me/p/%EB%A7%A8-%EC%95%84%EB%9E%98-%EA%B1%B0%EB%B6%81%EC%9D%B4-%EC%A0%9C%EB%84%A4%EC%8B%9C%EC%8A%A4-%EA%B3%84%EC%A0%95%EA%B3%BC-%EC%8B%A0%EB%A2%B0%EC%9D%98-%EB%BF%8C%EB%A6%AC/</link><pubDate>Mon, 06 Jul 2026 11:00:00 +0900</pubDate><guid>https://blog.j2eff.me/p/%EB%A7%A8-%EC%95%84%EB%9E%98-%EA%B1%B0%EB%B6%81%EC%9D%B4-%EC%A0%9C%EB%84%A4%EC%8B%9C%EC%8A%A4-%EA%B3%84%EC%A0%95%EA%B3%BC-%EC%8B%A0%EB%A2%B0%EC%9D%98-%EB%BF%8C%EB%A6%AC/</guid><description>&lt;!--
제목 후보 (Jeff 선택용):
 1. 맨 아래 거북이 — 제네시스 계정과 신뢰의 뿌리 ← 현재 가안
 2. 최초의 루트는 누가 만드나
 3. 신뢰는 어디서 끝나는가
--&gt;
&lt;p&gt;6편에서 슬쩍 넘어간 게 있다. &amp;ldquo;씨앗 계정&amp;quot;이라 부르며 &amp;ldquo;하드코딩된 특별한 존재&amp;quot;라고만 하고 지나갔다. 이번 편은 그 씨앗의 정체를 밝히고, 거기서 시작된 질문을 끝까지 따라간다. &lt;strong&gt;최초의 루트는 대체 누가 만드는가?&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id="씨앗의-진짜-이름"&gt;씨앗의 진짜 이름
&lt;/h2&gt;&lt;p&gt;씨앗은 아무 계정이 아니었다. 그건 &lt;strong&gt;제네시스 계정&lt;/strong&gt; — 모든 것이 거기서 파생되는 최초의 관리 계정이다. mincloud에서 그 번호를 &lt;code&gt;000000000001&lt;/code&gt;로 못박았다. 지금까지 &lt;code&gt;user/jeff&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;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&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="c1"&gt;// 제네시스 &amp;#39;관리 계정&amp;#39;과 그 루트를 심는다 — 어떤 서명된 요청보다도&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="c1"&gt;// 먼저 존재해야 하는 단 하나의 계정. AWS 가입 순간 루트가 존재하는 것과 같다.&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="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;loadManagementRoot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;credstore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Store&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;string&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="nx"&gt;account&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;cmp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Or&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;MINCLOUD_MANAGEMENT_ACCOUNT_ID&amp;#34;&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;000000000001&amp;#34;&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="nx"&gt;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;accessKeyID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;credstore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Credential&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="nx"&gt;SecretAccessKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;secret&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="nx"&gt;Identity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;credstore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Identity&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="nx"&gt;Account&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;account&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="nx"&gt;UserID&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;account&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// 루트의 UserId는 계정 ID&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="nx"&gt;ARN&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;arn:aws:iam::&amp;#34;&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;account&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;:root&amp;#34;&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="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="nx"&gt;accessKeyID&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;pre tabindex="0"&gt;&lt;code&gt;$ aws sts get-caller-identity --endpoint-url http://localhost:9900
{
 &amp;#34;UserId&amp;#34;: &amp;#34;000000000001&amp;#34;,
 &amp;#34;Account&amp;#34;: &amp;#34;000000000001&amp;#34;,
 &amp;#34;Arn&amp;#34;: &amp;#34;arn:aws:iam::000000000001:root&amp;#34;
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;이게 AWS의 &lt;strong&gt;관리 계정(management account)&lt;/strong&gt; 에 대응한다. 조직의 꼭대기 계정, 나머지 모든 계정이 그 아래에서 태어나는 뿌리다.&lt;/p&gt;
&lt;h2 id="그런데-그-루트는-누가-만들었나"&gt;그런데 그 루트는 누가 만들었나
&lt;/h2&gt;&lt;p&gt;여기서 6편의 순환이 한 층 더 깊어진다. 계정을 만들려면 계정이 있어야 한다고 했다. 그래서 부트스트랩은 서명 밖에 있어야 한다고. 좋다. 그런데 그 &lt;strong&gt;최초의 관리 계정 루트 자신&lt;/strong&gt;은 누가 만들었는가? 그것도 서명 밖에서 만들어져야 하는데, 그럼 그걸 만든 권한은 어디서 왔는가?&lt;/p&gt;
&lt;p&gt;무한히 위로 올라갈 것 같지만, 그렇지 않다. &lt;strong&gt;바닥은 다른 &amp;lsquo;키&amp;rsquo;가 아니다.&lt;/strong&gt; 거북이 탑은 물리적·조직적 통제에서 끝난다.&lt;/p&gt;
&lt;p&gt;mincloud의 경우, 제네시스 루트를 심는 건 &lt;code&gt;loadManagementRoot&lt;/code&gt;다. 그럼 그걸 실행할 권한은? &lt;strong&gt;바이너리를 실행할 수 있는 사람&lt;/strong&gt; — 이 박스를 통제하는 사람이다. 서명도 자격증명도 아니다. &amp;ldquo;기계에 손을 댈 수 있는가&amp;quot;가 최종 신뢰의 뿌리다.&lt;/p&gt;
&lt;p&gt;AWS도 원리는 같다. IAM·계정 시스템 자체가 AWS가 운영하는 서버 위의 소프트웨어고, 그 최초 상태는 &lt;strong&gt;API가 아니라 인프라 프로비저닝으로&lt;/strong&gt; 심긴다. 신뢰의 뿌리는 암호가 아니라 &lt;strong&gt;&amp;ldquo;데이터센터와 하드웨어를 물리적으로 소유·통제한다&amp;rdquo;&lt;/strong&gt; 는 사실이다. 소프트웨어 &lt;em&gt;위&lt;/em&gt;가 아니라 그 &lt;em&gt;아래&lt;/em&gt;에서.&lt;/p&gt;
&lt;h2 id="뿌리를-지키는-의식"&gt;뿌리를 지키는 의식
&lt;/h2&gt;&lt;p&gt;그 최종 루트를 어떻게 안전하게 다루는가 — 여기가 진짜 정교하다. 원리는 은행이든 DNS든 CA든 똑같다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;HSM&lt;/strong&gt;: 최상위 키가 탐침 방지 하드웨어 &lt;em&gt;안에서&lt;/em&gt; 태어나 &lt;strong&gt;밖으로 나오지 않는다.&lt;/strong&gt; 소프트웨어가 키를 &amp;lsquo;가지지&amp;rsquo; 않는다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;에어갭&lt;/strong&gt;: 루트 키는 네트워크에서 떼어 둔다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;M-of-N 분할 통제&lt;/strong&gt;: 한 사람이 루트를 쓸 수 없다. n명 중 k명이 각자의 조각(스마트카드)을 모아야 작동한다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;키 세리머니&lt;/strong&gt;: 최상위 키를 쓰는 건 다인원·기록·촬영·감사되는 &lt;em&gt;의식&lt;/em&gt;이다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;거의 쓰지 않는다&lt;/strong&gt;: 루트는 아래 계층에 위임하고 잠가둔다. 일상 작업엔 절대 나오지 않는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이게 추상이 아니다. &lt;strong&gt;DNSSEC 루트 키 세리머니&lt;/strong&gt;나 &lt;strong&gt;CA 루트 키 세리머니&lt;/strong&gt;를 찾아보면, 인류가 &amp;ldquo;위에 아무것도 없는 뿌리&amp;quot;를 만들고 지키는 방식이 문자 그대로 이렇다 — HSM, 다인원, 에어갭, 의식. (AWS 내부의 정확한 절차는 비공개지만, 이 산업 원리를 따른다.)&lt;/p&gt;
&lt;h2 id="mincloud가-진짜-서비스가-된다면"&gt;mincloud가 진짜 서비스가 된다면
&lt;/h2&gt;&lt;p&gt;지금 mincloud의 뿌리는 &amp;ldquo;로컬에서 바이너리를 실행하는 나&amp;quot;다. 이게 진짜 호스팅 클라우드가 되면, 그 뿌리는 &lt;strong&gt;사라지지 않고 옮겨간다&lt;/strong&gt; — &amp;ldquo;프로덕션 인프라와 시크릿 저장소를 통제하는 자&amp;quot;에게로.&lt;/p&gt;
&lt;p&gt;그때 반드시 바뀌는 것들:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;하드코딩 마스터 금지.&lt;/strong&gt; 지금의 well-known 기본값(&lt;code&gt;MINCLOUDTESTKEY0000A&lt;/code&gt;)은 로컬 편의일 뿐, 공개되면 백도어다. 프로덕션은 프로비저닝 때 &lt;strong&gt;유일한 루트를 생성&lt;/strong&gt;해 시크릿 매니저(KMS/Vault)에 넣는다. mincloud엔 이미 그 주입구가 있다 —&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;MINCLOUD_ROOT_ACCESS_KEY_ID / MINCLOUD_ROOT_SECRET_ACCESS_KEY
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;이미지엔 절대 안 넣고, 배포 시 시크릿으로만 주입한다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;부트스트랩 문은 인터넷에 노출하지 않는다.&lt;/strong&gt; 서명 없는 동작은 운영자 전용. 세상을 마주하는 건 서명된 API뿐이다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;시크릿 평문 저장 금지.&lt;/strong&gt; 지금 DynamoDB에 시크릿이 평문으로 들어가는 건 mock이라 넘어가는 것이고, 진짜라면 해시/KMS 암호화다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;그리고 stakes가 오를수록 뿌리를 더 조인다. 작은 서비스면 &amp;ldquo;시크릿 매니저 + MFA + 감사&amp;quot;로 충분하고, 은행급이면 아까의 HSM+세리머니까지 간다. 요는, &lt;strong&gt;진짜 서비스가 된다고 부트스트랩을 벗어나는 게 아니라, &amp;ldquo;누가 인프라를 통제하는가&amp;quot;를 더 형식화하고 강화할 뿐&lt;/strong&gt;이라는 것이다.&lt;/p&gt;
&lt;h2 id="다음-이제-누가-계정을-만드는가"&gt;다음: 이제 누가 계정을 만드는가
&lt;/h2&gt;&lt;p&gt;제네시스 루트가 섰다. 그럼 6편에서 아무나 열던 그 &amp;ldquo;계정 만드는 문&amp;quot;을 다시 볼 수 있다. 진짜 AWS에서 계정 생성은 아무 문이 아니다 — &lt;strong&gt;관리 계정의 권한&lt;/strong&gt;이다. 관리 계정이 서명해서, 그 아래 멤버 계정을 찍는다.&lt;/p&gt;
&lt;p&gt;다음 편에서 그걸 만든다. 서명 없는 문은 이제 제네시스 하나에만 남기고, 나머지 계정은 &lt;strong&gt;관리 루트가 서명한 &lt;code&gt;CreateAccount&lt;/code&gt;&lt;/strong&gt; 로만 태어나게 한다. 그리고 그 문 앞에서, 이 시리즈에서 처음으로 &lt;strong&gt;&amp;ldquo;인증은 됐는데 권한이 없다&amp;rdquo;&lt;/strong&gt; 는 대답을 만나게 된다.&lt;/p&gt;</description></item><item><title>최초의 계정은 어디서 오는가</title><link>https://blog.j2eff.me/p/%EC%B5%9C%EC%B4%88%EC%9D%98-%EA%B3%84%EC%A0%95%EC%9D%80-%EC%96%B4%EB%94%94%EC%84%9C-%EC%98%A4%EB%8A%94%EA%B0%80/</link><pubDate>Mon, 06 Jul 2026 10:00:00 +0900</pubDate><guid>https://blog.j2eff.me/p/%EC%B5%9C%EC%B4%88%EC%9D%98-%EA%B3%84%EC%A0%95%EC%9D%80-%EC%96%B4%EB%94%94%EC%84%9C-%EC%98%A4%EB%8A%94%EA%B0%80/</guid><description>&lt;!--
제목 후보 (Jeff 선택용):
 1. 최초의 계정은 어디서 오는가 ← 현재 가안
 2. 서명할 수 없는 문
 3. 계정을 찍어내다 — 부트스트랩
--&gt;
&lt;p&gt;5편까지 mincloud엔 계정이 딱 하나였다. 부팅할 때 심는 개발용 씨앗 자격증명. 그걸로 STS도 부르고 IAM 키도 발급했다. 그런데 그 씨앗을 안 쓰고 맨손으로 첫 키를 만들어 보려 하면, 벽에 부딪힌다.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;$ aws iam create-access-key --user-name friend01 \
 --endpoint-url http://localhost:9910

Unable to locate credentials.
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;당연하다. &lt;code&gt;create-access-key&lt;/code&gt;는 IAM 요청이고, IAM 요청은 SigV4로 서명해야 하고, 서명하려면 이미 자격증명이 있어야 한다. &lt;strong&gt;키를 만들려면 키가 있어야 한다.&lt;/strong&gt; 그럼 애초에 최초의 하나는 어떻게 존재하는가?&lt;/p&gt;
&lt;h2 id="서명할-수-없는-문"&gt;서명할 수 없는 문
&lt;/h2&gt;&lt;p&gt;답부터 말하면, 최초의 계정은 &lt;strong&gt;우리 API로 만들 수 없다.&lt;/strong&gt; 만들면 안 된다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;CreateAccount&lt;/code&gt; 같은 걸 SigV4 API에 넣었다고 하자. 그 요청에 서명하려면 키가 필요하고, 그 키는 계정이 있어야 나오고, 계정은… 순환이다. 그래서 AWS도 &lt;strong&gt;가입(sign-up)을 SigV4 API로 만들지 않았다.&lt;/strong&gt; 가입은 완전히 다른 평면이다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;서비스 평면&lt;/strong&gt; — sts, iam, ec2. SigV4로 인증한다. &lt;code&gt;aws&lt;/code&gt; CLI가 여기랑 대화한다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;가입/콘솔 평면&lt;/strong&gt; — 인증이 아니라 &lt;strong&gt;다른 신뢰&lt;/strong&gt;(이메일 확인·결제)로 계정과 루트를 계정 밖에서 찍어낸다. 여기엔 액세스키가 필요 없다 — 그걸 처음 &lt;em&gt;만드는&lt;/em&gt; 곳이니까.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;즉 부트스트랩은 서명의 바깥에 있어야 한다. mincloud도 똑같이 둘로 나눈다. 서비스 평면은 그대로 두고, 계정을 찍어내는 일은 &lt;strong&gt;서명하지 않는 별도의 문&lt;/strong&gt;에 둔다. 그 문의 신뢰는 서명이 아니라 &amp;ldquo;이 배포에 손을 댈 수 있는가&amp;quot;다.&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;span class="lnt"&gt;9
&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="c1"&gt;// admin 서브커맨드는 부트스트랩 평면이다. 서명하는 서비스 API가 아니라,&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="c1"&gt;// 최초 자격증명을 &amp;#39;만드는&amp;#39; 쪽. 그래서 SigV4를 거치지 않는다.&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="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;main&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;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&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;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Args&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&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="s"&gt;&amp;#34;admin&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&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="nf"&gt;runAdmin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Args&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&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&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="nf"&gt;runServer&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;지금까지 쓰던 씨앗 자격증명은, 사실 &amp;ldquo;이미 가입돼 있는 계정 하나를 미리 박아둔 것&amp;quot;이었다. 이제 그 문을 진짜로 연다.&lt;/p&gt;
&lt;h2 id="계정을-찍어내다"&gt;계정을 찍어내다
&lt;/h2&gt;&lt;p&gt;&lt;code&gt;admin create-account&lt;/code&gt;는 새 계정을 만든다. 계정 ID, 루트 유저, 그리고 그 계정만의 루트 액세스키/시크릿을 발급해 credstore(5편의 DynamoDB)에 저장하고, 운영자에게 건넨다.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;$ mincloud admin create-account
account created — hand the root credential to the account owner:
 Account: 340106438124
 Root AccessKeyId: AKIAE6L7MMXWPV4XWCNQ
 Root SecretKey: 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
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;(여기 값은 로컬에서 방금 만든 가짜다.) 이 문은 배포 안에서만 연다. 서명이 없으니 아무 데서나 두드리게 두면 안 되고, &amp;ldquo;박스에 접근할 수 있는 운영자&amp;rdquo; 가 신뢰의 경계다. 이게 AWS 콘솔 가입의 mincloud판이다 — 계정 밖 신뢰로 루트를 찍어내는 문.&lt;/p&gt;
&lt;p&gt;받은 루트키로 STS에 물으면, 신원이 돌아온다.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;$ aws sts get-caller-identity --endpoint-url http://localhost:9900
{
 &amp;#34;UserId&amp;#34;: &amp;#34;340106438124&amp;#34;,
 &amp;#34;Account&amp;#34;: &amp;#34;340106438124&amp;#34;,
 &amp;#34;Arn&amp;#34;: &amp;#34;arn:aws:iam::340106438124:root&amp;#34;
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;Arn&lt;/code&gt;이 &lt;code&gt;:root&lt;/code&gt;로 끝나고, &lt;code&gt;UserId&lt;/code&gt;가 계정 ID와 똑같다. 이게 루트의 표식이다. (뒤에서 IAM 유저와 대조한다.)&lt;/p&gt;
&lt;h2 id="12자리는-어디서-오나"&gt;12자리는 어디서 오나
&lt;/h2&gt;&lt;p&gt;여기서 멈칫할 수 있다. 저 &lt;code&gt;340106438124&lt;/code&gt;라는 12자리는 그냥 랜덤으로 뽑은 숫자인가? 그러면 계정 ID와 액세스키가 아무 관계 없는 두 난수가 된다. 그런데 &lt;strong&gt;진짜 AWS는 그렇게 안 한다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;잘 알려진 사실인데(Tal Be&amp;rsquo;ery, Aidan Steele의 연구), AWS 계정 ID는 &lt;strong&gt;액세스키 ID 안에 인코딩돼 있다.&lt;/strong&gt; &lt;code&gt;AKIA…&lt;/code&gt; 키만 있으면 API 호출 없이 오프라인으로 계정을 복원할 수 있다.&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;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;base64&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;account_of&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;raw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;base64&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;b32decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;:])&lt;/span&gt; &lt;span class="c1"&gt;# AKIA 뒤 16글자 → 10바이트&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;z&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;from_bytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;big&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;# 앞 6바이트(48비트)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="si"&gt;{:012d}&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;format&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;z&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="mh"&gt;0x7fffffffff80&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&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;mincloud도 이 방식을 그대로 따랐다. &lt;code&gt;create-account&lt;/code&gt;가 12자리를 정하면, 발급하는 액세스키가 그 계정을 base32 본문에 품도록 만든다. 그래서 우리가 만든 키를 &lt;strong&gt;진짜 공개 디코더에 넣어도&lt;/strong&gt; 우리 계정이 나온다.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;$ mincloud admin whoami AKIAE6L7MMXWPV4XWCNQ
AKIAE6L7MMXWPV4XWCNQ -&amp;gt; account 340106438124

$ python3 decode.py AKIAE6L7MMXWPV4XWCNQ
340106438124
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;계정이 &amp;ldquo;떠다니는 난수&amp;quot;가 아니라 &lt;strong&gt;키가 지니고 다니는 값&lt;/strong&gt;이 된 것이다. 눈으로도 보인다. 같은 계정에서 발급한 두 키는 앞부분을 공유한다.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;AKIAE6L7MMXW PV4XWCNQ ← 루트
AKIAE6L7MMXW D6RJRYGX ← 같은 계정의 다른 키
──────────── ────────
계정 인코딩 키별 랜덤
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;앞 열두 글자는 계정에서 파생돼 똑같고, 뒤는 키마다 다른 랜덤이다.&lt;/p&gt;
&lt;h2 id="루트가-유저를-만든다"&gt;루트가 유저를 만든다
&lt;/h2&gt;&lt;p&gt;이제 계정이 여럿이 될 수 있으니, 격리가 진짜 문제가 된다. 방금 만든 루트로 자기 계정 안에 IAM 유저 &lt;code&gt;alice&lt;/code&gt;를 발급해 보자.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;$ aws iam create-access-key --user-name alice --endpoint-url http://localhost:9910
...AccessKeyId: AKIAE6L7MMXWD6RJRYGX...

$ aws sts get-caller-identity --endpoint-url http://localhost:9900 # alice 키로
{
 &amp;#34;UserId&amp;#34;: &amp;#34;AIDAY7WVA4T7QWXELDV5&amp;#34;,
 &amp;#34;Account&amp;#34;: &amp;#34;340106438124&amp;#34;,
 &amp;#34;Arn&amp;#34;: &amp;#34;arn:aws:iam::340106438124:user/alice&amp;#34;
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;alice는 이 루트가 속한 계정(340106438124) 안의 유저다. 루트가 만든 키가 다른 계정으로 새지 않는다 — 자격증명마다 자기 계정을 품고 다니기 때문이다. alice의 키를 오프라인 디코드해도 같은 340106438124가 나온다.&lt;/p&gt;
&lt;p&gt;그리고 루트와 IAM 유저는 겉모습으로 구별된다.&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;정체&lt;/th&gt;
					&lt;th&gt;ARN 끝&lt;/th&gt;
					&lt;th&gt;UserId&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;루트&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;:root&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;계정 ID와 동일&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;IAM 유저&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;:user/&amp;lt;이름&amp;gt;&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;AIDA…&lt;/code&gt; 로 시작&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;code&gt;get-caller-identity&lt;/code&gt;가 돌려주는 이 한 줄이, &amp;ldquo;너가 루트냐 유저냐&amp;quot;를 그대로 말해 준다. 이 구분은 뒤에서 인가(authorization)를 다룰 때 핵심 열쇠가 된다 — 루트는 암묵적으로 다 되고, &lt;code&gt;:user/…&lt;/code&gt;는 정책이 있어야 뭔가 할 수 있다.&lt;/p&gt;
&lt;h2 id="두-평면-그리고-남은-빚"&gt;두 평면, 그리고 남은 빚
&lt;/h2&gt;&lt;p&gt;정리하면, mincloud는 이제 두 개의 문을 가진다. 서명하는 서비스 평면과, 서명하지 않는 부트스트랩 평면. 후자는 서명이 &lt;em&gt;없어야&lt;/em&gt; 한다는 게 핵심이다 — 서명을 붙이는 순간 최초의 계정을 만들 길이 다시 막힌다.&lt;/p&gt;
&lt;p&gt;이건 은근히 미끄러운 규칙이다. AWS CLI는 커스텀 모델(&lt;code&gt;aws configure add-model&lt;/code&gt;)로 새 명령을 붙일 수 있어서, 마음만 먹으면 &lt;code&gt;aws mincloud create-account&lt;/code&gt; 같은 걸 만들 수도 있다. 하지만 &lt;strong&gt;이 문만은 거기 두면 안 된다.&lt;/strong&gt; CLI 명령이 되는 순간 SigV4로 서명되고, chicken-and-egg가 부활한다. 부트스트랩은 끝까지 서명 바깥에 있어야 한다.&lt;/p&gt;
&lt;p&gt;정직하게 남는 빚도 있다. 씨앗 계정은 여전히 하드코딩된 특별한 존재고, &lt;code&gt;create-account&lt;/code&gt;는 지금 누구 손을 거쳐야 하는지(승인·쿼터)를 따지지 않는다. &amp;ldquo;박스에 접근하면 계정을 무한정 찍는다&amp;quot;는 건 데모니까 넘어가는 것이지, 진짜 가입 플로우는 그 앞에 결제와 검증이 있다.&lt;/p&gt;
&lt;p&gt;그런데 여기까지 오는 동안, 클라이언트는 늘 &lt;code&gt;aws&lt;/code&gt; CLI가 명령 하나를 던지는 식이었다. 다음 편에서는 훨씬 까다로운 손님을 부른다 — &lt;strong&gt;Terraform&lt;/strong&gt;. &lt;code&gt;plan → apply → destroy&lt;/code&gt;를 돌리는 이 손님은, 명령 하나로는 만족하지 않는다. mincloud의 반쪽짜리 IAM이 진짜 CRUD가 되어야 하는 순간이 거기서 온다.&lt;/p&gt;</description></item></channel></rss>