NameSpace 機能を使ったネットワークからインターネットに出れなかった話

コンテナのネットワーク周りを理解するためにカーネルの NameSpace 機能を使って Ubuntu20.04 サーバ上に仮想的なネットワークを構築してみた.その際,作成したネットワークからインターネットに出るのに四苦八苦したのでメモとして残しておく.この記事では,ネットワークの構築の詳細は話さず構築後のトラブルシュートをメインとする.

目次

概要

NameSpace 機能を使ったコンテナネットワークの実験をした.同じような実験についてはインターネットにいくつか記事があるが,今回は以下の記事を参考にした.

christina04.hatenablog.com

christina04.hatenablog.com

構築した環境は図のようになっている.

f:id:a-mochan:20211123122417p:plain
全体のネットワーク

上記ネットワークを構築するコマンドをまとめておいた.もしよければどうぞ.

クリックすると展開されます

#!/bin/bash

ip netns add host1
ip netns add host2
ip netns exec host1 ip link set lo up
ip netns exec host2 ip link set lo up
ip link add name veth1 type veth peer name br-veth1
ip link add name host1 type veth peer name br-host1
ip link add name host2 type veth peer name br-host2
ip link set host1 netns host1
ip link set host2 netns host2
ip link add br0 type bridge
ip link set dev br-veth1 master br0
ip link set dev br-host1 master br0
ip link set dev br-host2 master br0
ip addr add 10.0.0.100/24 dev veth1
ip netns exec host1 ip addr add 10.0.0.1/24 dev host1
ip netns exec host2 ip addr add 10.0.0.2/24 dev host2
ip netns exec host1 ip link set host1 up
ip netns exec host2 ip link set host2 up
ip link set veth1 up
ip link set br-veth1 up
ip link set br-host1 up
ip link set br-host2 up
ip link set br0 up

echo 1 > /proc/sys/net/ipv4/ip_forward
ip netns exec host1 ip route add default via 10.0.0.100
ip netns exec host2 ip route add default via 10.0.0.100

また,環境としては EC2 の Ubuntu20.04 を使ったので環境構築として以下の記事も参考にさせていただいた.また,EC2 の設定としてインターネットと通信できる状態にしておくことが前提となるのでご注意ください.

dev.classmethod.jp

こちらも Ubuntu で実行できるようコマンドをまとめておいた.

クリックすると展開されます

#!/bin/bash

apt-get update
apt-get install -y apt-transport-https ca-certificates curl software-properties-common jq
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | apt-key add -
add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable"
apt-get install -y docker-ce cgdb cgroup-tools uuid-runtime tree iputils-ping make gcc
git clone git://git.kernel.org/pub/scm/linux/kernel/git/morgan/libcap.git /usr/src/libcap
cd /usr/src/libcap
make
make install

NameSpace を使って隔離したネットワークからインターネットへ出ていけない

先に紹介した記事 にもある通り,NameSpace 機能で作成した Host1 や Host2 からインターネットに出ていくためには iptables による Nat を設定しなければならないのでホスト Linux 上で以下のように設定した.

iptables -t nat -I POSTROUTING -s 10.0.0.0/24 -o eth0 -j MASQUERADE

この状態で Host1 から ping を打ってみるが,インターネットへ疎通できなかった.

ip netns exec host1 ping 8.8.8.8
PING 8.8.8.8 (8.8.8.8) 56(84) bytes of data.
...

試しに eth0,veth1,br-veth1,br-host1 のそれぞれで tcpdump をしてみてどこまでパケットが届いているか確認すると br-host1 のキャプチャでしかパケットが表示されなかった.

tcpdump -i eth0 -p icmp -n
...
tcpdump -i veth1 -p icmp -n
...
tcpdump -i br-veth1 -p icmp -n
...
tcpdump -i br-host1 -p icmp -n
01:49:58.420877 IP 10.0.0.1 > 8.8.8.8: ICMP echo request, id 8735, seq 203, length 64
01:49:59.444910 IP 10.0.0.1 > 8.8.8.8: ICMP echo request, id 8735, seq 204, length 64

f:id:a-mochan:20211123122524p:plain
Nat設定状態でパケットが到達したNIC

どうやら bridge 内の通信がまずうまくいっていないので以下の記事を参考にして bridge 内通信を修正.今回は Netfilter で iptables を呼ばない設定をすることにした.記事の最後にこのカーネルパラメータを 1 の状態で解決できなかった理由も書いておく.

sysctl -w net.bridge.bridge-nf-call-iptables = 0

qiita.com

再度各 NIC に対して tcpdump をしてみたところ veth1 までパケットが届いていることがわかった.

tcpdump -i veth1 -p icmp -n
03:06:17.104353 IP 10.0.0.2 > 8.8.8.8: ICMP echo request, id 9766, seq 1, length 64

f:id:a-mochan:20211123122701p:plain
Netfilter の設定変更後にパケットが到達した NIC

tcpdump より veth1 から eth0 へのパケットが落ちていることが分かるので,iptables で veth1 から eth0 への FORWARD を設定した.

iptables -t filter -I FORWARD -s 10.0.0.0/24 -d 0.0.0.0/0 -j ACCEPT

eth0 の NICtcpdump すると Nat の設定も効いていて,インターネットから eth0 まで通信が返ってきていることを確認できた.

tcpdump -i eth0 -p icmp -n
03:12:11.326420 IP 172.30.20.98 > 8.8.8.8: ICMP echo request, id 9779, seq 1, length 64
03:12:11.328700 IP 8.8.8.8 > 172.30.20.98: ICMP echo reply, id 9779, seq 1, length 64

最後に eht0 から veth1 への戻りの FORWARD 設定をする.セキュリティも考慮してソース IP は 8.8.8.8 だけにしておく.

iptables -t filter -I FORWARD -d 10.0.0.0/24 -s 8.8.8.8/32 -j ACCEPT

そうすると Host1 からインターネットへの接続を確立できる.

ip netns exec host2 ping 8.8.8.8
PING 8.8.8.8 (8.8.8.8) 56(84) bytes of data.
64 bytes from 8.8.8.8: icmp_seq=1 ttl=104 time=2.51 ms
64 bytes from 8.8.8.8: icmp_seq=2 ttl=104 time=2.36 ms

余談

今回は bridge ネットワーク通信を正常にするために net.bridge.bridge-nf-call-iptables を 0 に設定した対応を取ったが,先の記事 にある iptables -I FORWARD -m physdev --physdev-is-bridged -j ACCEPTiptables に設定する方法で進めてみた.
うまくいかない箇所は同じで,まずは veth1 から eth0 への通信が失敗するので iptablesiptables -t filter -I FORWARD -s 10.0.0.0/24 -d 0.0.0.0/0 -j ACCEPT を設定する.そうすると eth0 にパケットが到達するようになるが,なぜか Nat が効かない状態となる.この Nat をどうにか効かせようと色々調べて設定も変えてみたがうまくいかなかった.結果として net.bridge.bridge-nf-call-iptables=0 にする方法でうまくいったのでそちらをまとめたのだが,もし誰かこの原因をご存知の方がいたらコメント欄に書いていただけると助かります.

まとめ

  • トラブルシュートしながらコンテナネットワークからインターネットへ出ていく時の設定を見つけた
  • 改めてネットワークトラブルは tcpdump などでパケットを取り続けるしかないことを学んだ
  • net.bridge.bridge-nf-call-iptables の設定をそのままにした状態でうまくいかない原因は明確となっていない

サーチリストを使った名前解決はやめよう

DNS を運用していると名前衝突問題に気を付ける必要がある.名前衝突問題とは,組織が内部的に使う Top Level Domain(TLD)と インターネットで利用できる TLD が重複してしまうことによりDNSの動作が期待するものとは違った動作になることを指す.名前衝突問題で具体的に問題となるのは,例えばインターネット上のドメイン名を検索するつもりが,ローカルネットワークで独自に付けたTLDに対して名前解決を行ってしまうことや,またその反対に,ローカルネットワークのドメイン名を検索するつもりがインターネット上の TLD に対して名前解決してしまうことである.JPNIC のページが詳しいのでリンクを置いておく.

www.nic.ad.jp

今回は後者の問題である「ローカルネットワークのドメイン名を検索するつもりがインターネット上の TLD に対して名前解決してしまう」という事象を再現してみようと思う.上記の記事で紹介されているサーチリストを使ったケースを想定する.

概要

環境として使うのは AWS とし,AWS の各サービスの説明は本質的な部分とは異なるので今回は割愛する.

dig コマンドで名前解決の確認を行うために OS を Ubuntu18.04 とした EC2 を立てておく.EC2 から利用する内部的に使うドメインは Route53 で管理し, example.com という名前でプライベートホストゾーンを作成する.プライベートホストゾーンにインターネット上に存在するサブドメインを登録し,サーチリストを設定した EC2 から名前解決を行うと「ローカルネットワークのドメイン名を検索するつもりがインターネット上の TLD に対して名前解決してしまう」が再現することを確認する.

Route53 の設定

Route53 では example.com というプライベートホストゾーンを作成し,適当なプライベート IP アドレス(172.30.20.86)を指定した 2 つの A レコードを登録しておく.

aws という TLDAWS が取得しているドメインであり, dns1.nic.aws を名前解決すると既に A レコードを正引きすることができる.

Route53 の設定を以下の図に示す.

f:id:a-mochan:20211031174115p:plain
Route53の設定

サーチリストの設定

EC2 でサーチリストを設定する.

サーチリストは、DNS検索サフィックスDNS suffix search listなどとも呼ばれる、 ユーザーがドメイン名を入力する手間を減らせるようにするためのリストです。 具体的にはDNSにおいて、 名前解決の際にドメイン名を最後まで入力しなくても、 サーバやクライアントで補完がされるように、 補完候補となる文字列を順番に並べたものです。 www.nic.ad.jp

Ubuntu18.04 の場合 EC2 上で /etc/netplan/99-manual.yaml というファイル名を以下の内容で作成し再起動すればよい.

network:
    ethernets:
        eth0:
            nameservers:
                search:
                  - example.com
    version: 2

再起動した EC2 にログインし,サーチリストを使うようにオプションをつけた dig を実行すると example.com が補完され instance-dst.example.com を名前解決できていることが確認できる.

root@ip-172-30-20-203:/etc# dig instance-dst +search

; <<>> DiG 9.11.3-1ubuntu1.16-Ubuntu <<>> instance-dst +search
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 41197
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 65494
;; QUESTION SECTION:
;instance-dst.example.com.  IN  A

;; ANSWER SECTION:
instance-dst.example.com. 300   IN  A   172.30.20.86

;; Query time: 2 msec
;; SERVER: 127.0.0.53#53(127.0.0.53)
;; WHEN: Sun Oct 31 08:55:09 UTC 2021
;; MSG SIZE  rcvd: 69

では,プライベートホストゾーンに登録してあるもう 1 つのサブドメインdns1.nic.aws を名前解決してみる.

root@ip-172-30-20-203:/etc# dig dns1.nic.aws +search

; <<>> DiG 9.11.3-1ubuntu1.16-Ubuntu <<>> dns1.nic.aws +search
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 8193
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 65494
;; QUESTION SECTION:
;dns1.nic.aws.          IN  A

;; ANSWER SECTION:
dns1.nic.aws.       300 IN  A   213.248.218.53

;; Query time: 2 msec
;; SERVER: 127.0.0.53#53(127.0.0.53)
;; WHEN: Sun Oct 31 08:58:35 UTC 2021
;; MSG SIZE  rcvd: 57

名前解決の結果 172.30.20.86 ではなく 213.248.218.53 が返ってきている.これより,プライベートホストゾーンではなくインターネットで公開されているドメインが正引きされていることが分かる.これは,サーチリストの補完が行われる前にまずは dig コマンドで指定されたドメインが名前解決されるため, dns1.nic.aws の正引き結果が返ってきてしまっている状態である.この事象は,プライベートホストゾーンのサブドメインを運用中にそのサブドメインとインターネットに存在するドメインが重複した場合,サーチリストを設定したサーバからの名前解決が失敗する可能性があることを示している.

対策

JPNIC が紹介している対策はシンプルでサーチリストを使わないことである.

名前衝突の問題への根本的な対策は、TLDの重複を避けることです。つまり、原因となっている、内部向けのTLDやサーチリストの使用を止めることです。 www.nic.ad.jp

サーチリストを使わなければ名前解決する際に必ずプライベートホストゾーンの FQDN 指定するので,今回の事象は発生しなくなる.

まとめ

  • DNS の名前衝突による「ローカルネットワークのドメイン名を検索するつもりがインターネット上の TLD に対して名前解決してしまう」問題の再現をした
  • 対策であるサーチリストを使わないということが大事

別AWSアカウントへIAMロールを提供する際は外部IDを思い出す

IAMロールでスイッチロール(マネージメントコンソールで他のAWSアカウントのロールにスイッチすること)を実現する際に「外部ID」というオプションがあったので,何のためにあるのかまとめてみた.先に言っておくとスイッチロールを使うときに外部IDを気にする必要はない

外部IDとは

外部IDとは,第三者用のIAMロールを作成する際にセキュリティ対策用途で設定する値のことである.このセキュリティ対策は「混乱した代理」と呼ばれる問題を解決するために実施される.「混乱した代理」の概要は以下に解説されているが,この記事を通して理解できるかと思う.

docs.aws.amazon.com

以下は実際のIAMロールを作成する画面であり,外部IDを設定できることが分かる.スイッチロールを使うときに外部IDを指定する必要はない旨もかかれている.

f:id:a-mochan:20210605165605p:plain

コンソールでは、ロールの切り替え機能を使用する外部 ID の使用はサポートされていません。 画像にも書いてある通り,スイッチロールさせたいだけなら外部IDは無視して構わない.

ユースケース

スイッチロールで使わないのであれば,どんなユースケースがあるのかを考えてみる.また「混乱した代理」とその対策である外部IDがどのように機能するのか理解する.

異なる AWS アカウントで複数の顧客をサポートするマルチテナント環境では、AWS アカウントごとに 1 つの外部 ID を使用することをお勧めします。

「混乱した代理」の概要より,複数のAWSアカウントをお客様に持つサービスを提供している会社を例にとると理解しやすい.ここでは「a-mochan」という監視系のSaaSサービスがあると仮定してユースケースを説明する.「a-mochan」もAWS環境でサービス提供をしている.

「a-mochan」では,お客様のS3バケットにあるログへ定期的にアクセスし,お客様ごとにログを可視化するダッシュボードを用意している.「a-mochan」のお客様であるClient Aは自身のAWSアカウントのS3に「a-mochan」からのアクセスを許可するためにIAMロールを作成した.その際外部IDは設定しなかった.Client A は作成したIAMロールのAmazon Resource Name(arn)を「a-mochan」上で設定し,S3のデータを「a-mochan」に取り込めるようにしたことでダッシュボードを確認することができた.

f:id:a-mochan:20210617212756j:plain
クライアントとサービスの関係

ここで,悪意のあるユーザが「a-mochan」を利用し始めた.悪意のあるユーザは,IAMロールのarnが arn:aws:iam::[AWS AccountID]:role/[Role Name] のような形で推測が容易なこともあり,Client A が作成したIAMロールのarnを推測し「a-mochan」上で設定することができた.そうすると悪意のあるユーザのダッシュボードにも Client A のS3の情報が表示され,悪意のあるユーザは Client A のダッシュボードを閲覧可能な状態になる.この状態のことを「混乱した代理」と呼ぶ.

f:id:a-mochan:20210617210024j:plain
混乱した代理が発生

では「混乱した代理」対策のために外部IDを利用する流れをみていく.「a-mochan」はお客様ごとに一意のIDを発行し,お客様は作成するIAMロールの外部IDとして「a-mochan」が発行したIDを指定する.そうするとClient AのAWSアカウントでは「a-mochan」からのアクセス時に外部IDが一致しているかどうかチェックするようになるので,「a-mochan」ではClient AのAWSアカウントへのリクエスト時に外部IDを付与する仕様に変更する.外部IDは「a-mochan」のシステムで発行された利用者ごとに一意なIDで,利用者はコントロール不可能な値である.そのため,悪意のあるユーザが Client A のIAMロールのarnを推測できたとしても,「a-mochan」から発行されたClient A用のID(948833852)と悪意のあるユーザ用のID(749678483) が違うので「混乱した代理」は起こらない.

f:id:a-mochan:20210617210030j:plain
外部IDにより混乱した代理が起こらない

このように外部IDは「混乱した代理」を引き起こさないために設定する値のことであることがわかった.

感想

AWSアカウントへIAMロールを用いてアクセスさせるようなサービスを利用する時は,外部IDの発行が実装されているか確認すべきだと感じた.外部IDを考慮していないサービスはどのような方法で「混乱した代理」を回避しているのか確認して利用するサービスを選定すべきなのかなと思う.また逆も然りで,そのようなサービスを提供する時は外部IDを考慮した実装にすべきである.

まとめ

  • 外部IDの意図を理解した
  • 外部IDが必要となる「混乱した代理」についてユースケースを用いて理解した

A10のリバースプロキシで外部サーバへ転送する設定

A10の調べ物で色々ググるがあまり出てこないので,今回はA10でのリバースプロキシの設定をまとめておこうと思う.
A10のハードウェアアプライアンスA10 Thunder 4430である.

現状確認と要件

オンプレ環境にLBとしてA10を設置しており,A10ではインターネット(エンドユーザ)からのトラフィックを配下のLANに流している.仮にhoge.co.jpというサービスをオンプレ環境にホストしているとして,https://hoge.co.jp/fuga.txtというファイルにアクセスされたらそのトラフィックを外部のサーバに流し,それ以外のパスについては従来通り配下のLANに流すような要求が発生したとする.外部サーバにはhttpsで通信し,外部サーバのFQDNpiyo.co.jpのようにhoge.co.jpとは異なることを想定している.今回の外部サーバはAWSにあるものとし,443番ポートを受け付けているALB(Application Load Balancer)とそこに80番ポートを受け付けているEC2がぶら下がっている構成にする.イメージを下図に示す.

f:id:a-mochan:20201218234415p:plain
構成図

上記の要件を実現するためにA10で実施すべきことの詳細を説明する.

SNATの必要性

上記の通信を実現するには,A10で外部からのリクエストを受け取ったとき,ネットワーク層のソースIPをNAT(SNAT)させる必要がある.なぜなら,SNATをしないと外部サーバへたどり着くリクエストのソースIPはクライアントのものになり,外部サーバからのレスポンスがA10を経由せずクライアントに直接返されてしまうからである.クライアントからするとリクエストはA10に送信したのに,レスポンスは外部サーバから受け取ってしまう形となり通信ができない状態となる.したがってA10ではSNATも考慮する.

復号化と再暗号化

A10では受け取ったリクエストのパスで振り分け先を判断するので,一度復号しないとパスを確認することができない.また,A10・外部サーバ間はhttpsの通信を想定している.したがってA10では,受け取ったリクエストのアプリケーション層の情報(主にパス)を確認するために一度復号化し,再度暗号化して外部サーバへ通信する必要がある.

ホストの書き換え

A10配下のLANにホストしてあるFQDNhoge.co.jpで,外部サーバにホストしてあるFQDNpiyo.co.jpを想定しているので,A10では外部サーバへ転送する際にホストを書き換える必要がある.

実現方法

上記で説明してきた要件をA10では以下の手順で設定していく.

  1. ALBをサーバとして登録,サーバをサービスグループとして登録
  2. IP Source Pool作成
  3. Client SSL,Server SSLテンプレート作成
  4. aFlex作成
  5. バーチャルサーバ作成

1. ALBをサーバとして登録

外部サーバとしてA10にALBを登録する.外部サーバの登録方法としてIPかFQDNを選べるので今回はALBから払い出されるDNS名を指定する.ヘルスチェックは外部サーバによって決めればよいが,今回は443ポートチェックで行う.作成したサーバをサービスグループとして登録する.

slb server piyo-alb piyo-alb-1111111111.ap-northeast-1.elb.amazonaws.com
  port 443 tcp
    health-check tcp_443
slb service-group piyo-sg tcp
  member piyo-alb 443

2. IP Source Pool作成

SNATのためのIPプール作成.

ip nat pool global_ip_pool 33.44.55.1 33.44.55.10 netmask /24 ip-rr

3. Client SSL,Server SSLテンプレート作成

復号化・暗号化するためのClient SSL,Server SSLテンプレート作成.秘密鍵や証明書の登録は割愛する.

slb template client-ssl any.co.jp
  ca-cert CA
  cert any.co.jp
  key any.co.jp
slb template server-ssl reverse-proxy-re-encrypt

4. aFlex作成

A10のリバースプロキシを実現するキモとなるのがこのaFlexである.1~3で設定した内容をaFlexのスクリプトで定義する.また,ホストの書き換えもaFlexで行う.reverse-proxy-aflexという名前で作成する.

when HTTP_REQUEST {
  if { [HTTP::path] matches_regex "^/fuga.txt$"} {
    HTTP::header insert X-Forwarded-For [IP::client_addr]   # クライアントのIPを外部サーバのアプリケーションログに出したければこの設定を入れる
    HTTP::header Host "piyo.co.jp"
    pool piyo-sg
    snatpool global_ip_pool
    SSL::template serverside reverse-proxy-re-encrypt
  }
}

5. バーチャルサーバ作成

/fuga.txtの場合はaFlexで処理し,それ以外の場合はLANに流すサービスグループを指定したバーチャルサーバを作成.

slb virtual-server reverse-proxy-vs 111.222.11.22
  port 443 https
    aflex reverse-proxy-aflex
    service-group hoge-sg   # `/fuga.txt`ではないパスの場合はLANに流すようにサービスグループを設定
    template client-ssl any.co.jp   #復号化

以上の設定でhttps://hoge.co.jp/fuga.txtにアクセスするとALBに転送され,その他のパスについてはLANに流れるようになる.

まとめ

  • A10のリバースプロキシで外部サーバへ転送した
  • 今回はインターネットを経由するパターンで設定したが,専用線などが引いてありインターネットを経由しない場合は再暗号化しなくてもよいと思われ,今回の設定からいくつかステップを省略することが考えられる
  • A10を使用しているオンプレからAWSへ移行する際には使用する場合があるかもしれない

CloudFormation StackSetsを使う上で覚えておきたいこと

業務でCloudFormationのStackSets(以降StackSets)という機能を使ってマルチアカウント・マルチリージョンデプロイを試してみて、いくつか気をつけるべき点があったのでメモがてらまとめておこうと思う.

StackSetsについて

StackSetsを簡単に説明する.StackSetsとは,複数のAWSアカウントや複数のリージョンに跨ってAWSサービスをデプロイする時に使用するサービスである.単一のアカウントにデプロイするサービスとしてCloudFormation Stackがあるが,その拡張版といってよい.StackSetsでは,StackSetsを実行するアカウントとStackSetsによってサービスがデプロイされるアカウントが存在する.前者を親アカウント,後者を子アカウントと呼ぶことにする.親アカウントでStackSetsを実行すると,子アカウントにスタックが作成され,そのスタックのテンプレートに記載されている内容が子アカウント上で設定される.

https://d2908q01vomqb2.cloudfront.net/972a67c48192728a34979d9a35164c1295401b71/2019/09/04/6955-1-CloudFormation-StackSets.png

StackSetsを実行すると親アカウントにスタックセットと呼ばれる子アカウントのスタックを管理するリソースが作成され,そのスタックセット配下には各アカウントの各リージョン毎に一意なスタックインスタンス(子アカウントのスタックへのリファレンス)が作成される.詳しくは公式ページを参照あれ.

docs.aws.amazon.com

StackSetsを使う際の注意点

AWS CLIのバージョンの違いによるaws cloudformation describe-stack-setの表示項目の違い

aws cloudformation describe-stack-setは作成したスタックセットの詳細を取得するコマンドである.AWS OrganizationsのOrganization Unit(以降OU)を使ったデプロイを使っている場合,aws cloudformation describe-stack-setを実行することで,OrganizationalUnitIdsの項目に対象のスタックセットがどのOUにデプロイされているかが表示される.しかし,古いAWSコマンドのバージョンによってはOrganizationalUnitIdsという項目が表示されない事象がある.どのバージョンまでが表示されなくて,どのバージョンから表示されるかは分からないが,自分の環境のバージョンで試した結果は少なくともaws-cli/1.16.250では表示されず,aws-cli/1.18.125では表示された.OUを使ったデプロイが今年に入ってからの機能なので,AWSコマンドの古いバージョンを使っている場合は注意する.

aws.amazon.com

親アカウントが属しているOUにデプロイするとき,親アカウント自身にはデプロイされない

OUを使うデプロイを考えた時,子アカウントだけでなく親アカウントもOUに含めることで親アカウントにもデプロイするよう試してみたが,親アカウントにはデプロイできなかった. これは公式ページにも記載されている.親アカウントにもデプロイしたい場合は別途StackやStackSetsを使ってデプロイしなければならない.

StackSets は、マスターアカウントが組織内または組織の OU 内にあっても、スタックインスタンスを組織のマスターアカウントにデプロイしません。

このページの「サービスマネージド型のアクセス許可を持つスタックセットを作成する際の考慮事項」はOUを使ったデプロイを想定している方はよく読んでおくとよいかもしれない. docs.aws.amazon.com

マルチリージョンにデプロイする際,グローバルリージョンにしか存在しないリソースの設定には気をつける

マルチリージョンにサービスをデプロイする前提で,グローバルリージョンにしか存在しないサービス(例えばIAM)をStackSetsで設定する場合は気をつけなければならない.例えば,ap-northeast-1のリージョンでIAMロールを作成し,us-east-1のリージョンでもIAMロールを作成した時,作成した2つのIAMロール名が同じ場合はエラーになる.この対応としては「どこか1つのリージョンで設定する」もしくは「IAMロール名をリージョン毎に変える」等で対応できる.これは当たり前といえば当たり前だが,意外とグローバルリージョンにしか存在しないサービスはあるので頭の片隅に置いておくとよいと思う.

サービスによってはクロスリージョンの通信ができないことによってStackSetsの実行が失敗する場合がある

サービスによってはクロスリージョンの通信ができないことによりStackSetsの実行が失敗するものがあるのでそれらを考慮する必要がある.
例えば,ap-northeast-1に作成したAWS Configで通知をAmazon Simple Notification Service(以降SNS)に送信したい時,SNSap-northeast-1に作成されている必要がある.これを例えばus-east-1に作成したSNSに通知しようとするとエラーとなる.他にも例えば,LambdaをStackSetsでデプロイする際,ap-northeast-1に作成されたLambdaのアーティファクトS3はap-northeast-1に作成されている必要がある.これを例えばus-east-1に作成したアーティファクトS3からコードを取得しようとするとエラーとなる.
このように,サービスによってはクロスリージョンアクセス不可能な設定があるので,それぞれのリージョンで必要なサービスを配置しておかないとStackSetsを流した時にエラーになることがある.
個人的な見解になるが,サービスによってクロスリージョンアクセスする・しないを分けるのはややこしいので,特に強い理由がなければ連携するサービスは1つのリージョンにおさめるのが分かりやすくて良いと思っている.

まとめ

  • StackSetsを使う上での注意点を列挙した
  • OUと連携できるようになってからStackSetsは余計な設定をすることなく使えるようになったので重宝しそう

JumpCloudの鍵認証で使用されるauthorized_keys.jcorigを理解する

業務上AWSのEC2のユーザ管理をJumpCloudで統一して管理している.JumpCloud上で管理対象のEC2に鍵認証を許可することができるのだが,公開鍵を設定する方法として「JumpCloudのコンソール上で設定する方法」と「実際にサーバに手動で設定する方法」がある.後者で設定しようとした際に~/.ssh/authorized_keysの他に~/.ssh/authorized_keys.jcorigというファイルがあることに気づいた(以降authorized_keysauthorized_keys.jcorigと表記する).今回はこれがなんのためにあるものなのか調べてみた.

authorized_keys.jcorigとは

公式には詳細については書いていなかった. support.jumpcloud.com

実際にauthorized_keys.jcorigの中身を確認したところ

# Keys added to this file will be automatically
# copied to your authorized_keys file by the JumpCloud agent.

と書いてあり,authorized_keys.jcorigに書くとJumpCloudエージェントがauthorized_keysにコピーしてくれるようだ.

さらにauthorized_keysを確認すると

# Keys added to this file will be automatically
# copied to your authorized_keys file by the JumpCloud agent.

# Constructed by the JumpCloud agent, you can add keys manually in /home/hoge/.ssh/authorized_keys.jcorig

と書いてあり,サーバ上で手動で設定するならauthorized_keys.jcorigに書き込めという旨が書いてある.

検証

公式にそこまで情報はないがauthorized_keys.jcorigに書き込めばauthorized_keysにJumpCloudエージェントが内容を反映してくれるのだろうという予想はできたので,実際に色々と試して検証してみることにする.JumpCloudエージェントがインストールされたEC2を用意し以下を確認していく.

  • サーバ上に手動で設定するパターン
    • authorized_keys.jcorigに公開鍵を追加
    • authorized_keysに公開鍵を追加
  • JumpCloud上で設定するパターン
    • 公開鍵を追加
    • 公開鍵を削除

サーバ上でauthorized_keys.jcorigに手動で公開鍵を追加

まずは正当な方法であろうauthorized_keys.jcorigに手動で公開鍵を追加する.しかし,しばらくしてもauthorized_keys.jcorigの内容がauthorized_keysにコピーされずログインはできなかった.そこで以下3パターンでログインの可否を確認する.

  • EC2を再起動するとauthorized_keys.jcorigの内容がauthorized_keysにコピーされログインできた
  • authorized_keysにも公開鍵を記述するとログインできた(その後EC2を再起動しても特に問題なし)
  • JumpCloudエージェントを再起動する方法ではauthorized_keysに内容がコピーされることはなくログインできなかった

サーバ上でauthorized_keysに手動で追加

次にauthorized_keysに公開鍵を追加してみた.するとログインできるようになったが,EC2を再起動をするとauthorized_keysから公開鍵が消えてしまいログインできなくなった.

ここまでの検証で以下のことがわかる.

  • 基本的にはauthorized_keysに公開鍵が登録されているかどうかで認証の機能が動作する一般的なLinuxの仕組みと同じ動きをする
  • authorized_keys.jcorigに公開鍵を記述してもauthorized_keysに記述されないと鍵認証されないので,即時でログインしたい場合はEC2を再起動するauthorized_keysにも同じ内容を書くことが必須
  • 手動で公開鍵を追加するにはauthorized_keys.jcorigに書かないと永続化されない
  • 公開鍵を削除する際は少なくともauthorized_keys.jcorigの公開鍵を削除しないと,EC2を再起動した際に再びauthorized_keysに書き込まれてしまう
  • 公開鍵を削除する際,即時にログインできないようにするにはauthorized_keys.jcorigの公開鍵を削除してEC2を再起動するかauthorized_keys.jcorigauthorized_keys両ファイルから公開鍵を削除する必要がある
  • authorized_keys.jcorigの内容をauthorized_keysにコピーする方法として,JumpCloudエージェントの再起動は使えない
  • 公開鍵の追加・削除共にauthorized_keys.jcorigが正として振る舞っている

JumpCloudコンソールから公開鍵を追加

JumpCloudコンソールを使って公開鍵を追加すると,EC2を再起動しなくともしばらくするとauthorized_keysに公開鍵が追加される.一方authorized_keys.jcorigには何も追加されない.また公開鍵を手動で追加した場合とJumpCloudコンソールから追加した場合とでauthorized_keysに記述される公開鍵の位置が以下のように異なる.

$ cat authorized_keys
# Keys added to this file will be automatically
# copied to your authorized_keys file by the JumpCloud agent.
[authorized_keys.jcorigからコピーされた手動で設定した公開鍵]

# Constructed by the JumpCloud agent, you can add keys manually in /home/yuichiro.takarada/.ssh/authorized_keys.jcorig
[JumpCloudコンソールで設定した公開鍵]

JumpCloudコンソールから公開鍵を削除

JumpCloudコンソールを使って公開鍵を削除すると,EC2を再起動しなくともしばらくするとauthorized_keysから公開鍵が削除される.ちなみに,JumpCloudコンソールから設定されたauthorized_keysに記述してある公開鍵を手動で削除するともちろんログインはできなくなるがJumpCloudのコンソールからは削除されない.そこからEC2を再起動するとauthorized_keysにJumpCloudコンソールで設定した公開鍵が再び設定される.

ここまでの検証で以下のことが分かる.

  • JumpCloudコンソールを使って追加された公開鍵はauthorized_keys.jcorigに書き込まれず,authorized_keysにのみ書き込まれる
  • JumpCloudコンソールを使って追加・削除された公開鍵は,EC2を再起動せずとも自動でauthorized_keysに追加・削除が行われる
  • JumpCloudコンソールを使って追加された公開鍵がauthorized_keysからサーバ上で手動により削除された場合でも,JumpCloudコンソールから削除されていなければ,EC2の再起動後再びauthorized_keysに書き込まれる
  • JumpCloudコンソールの状態を正として振舞う

検証結果からの学び

検証結果から,JumpCloudエージェントが入ったインスタンスでは「サーバ上で手動による公開鍵を設定する」・「JumpCloudで公開鍵を設定する」という2つの公開鍵設定方法がある中で,前者の方法ではauthorized_keys.jcorigを正としてauthorized_keysに公開鍵をコピーし,後者の方法ではJumpCloudコンソールを正としてauthorized_keysに公開鍵をコピーしていることが分かった.運用においては基本JumpCloudコンソール上から設定する方法でなんら問題ないと思うのでそうするつもりだが,どうしても手動で設定する必要がある場合だけ今回の調査を生かしてauthorized_keys.jcorigに公開鍵を記述する運用にしたい.とにかく,authorized_keys.jcorigが「サーバ上で手動による公開鍵を設定する」ためのファイルだということは理解した.

まとめ

  • JumpCloudエージェントが入ったインスタンスauthorized_keys.jcorigというファイルがなんのためにあるのかということを調べた
  • authorized_keys.jcorigは「サーバ上で公開鍵を設定する」場合の公開鍵を管理するファイルであることが分かった

LambdaとSSMでEC2からS3のオブジェクト取得させる

AWS環境で,Lambdaを用いてS3からEC2にオブジェクトを取得させる設定をしたのでメモがてら残しておく.

概要

Lambdaを使い,AWS Systems Manager(SSM)経由でEC2にAWSコマンドを実行させ,S3からオブジェクトを取得する.また,S3にあるオブジェクトは他AWSアカウントからPUTされたと想定する.以下に図を示す.

f:id:a-mochan:20200312213744j:plain

S3のバケットポリシーの設定

AWSアカウント間をまたぐS3バケットへのアップロードでは,オブジェクトの権限に気をつける.
例えば,AというAWSアカウントがS3バケットを作成したとして,Bという他のAWSアカウントからS3バケットへオブジェクトをPUTする.PUTする際にオプションを何も指定しなければ,S3バケットへPUTされたオブジェクトの所有者はBとなってしまい,AにあるEC2からはGETできないしマネージメントコンソールからもダウンロードできない.これを避けるため,PUTする際(aws-cliの場合)に--acl bucket-owner-full-controlオプションをつけて実行する.これにより,PUTしたオブジェクトの所有者がAアカウントになりAアカウントからGETすることが可能となる.詳しくは以下のブログが参考になった.

qiita.com

test-bucketというS3バケットを用意し,他のアカウントからPUTしてもらう.PUTしてもらう際に--acl bucket-owner-full-controlオプションをつけておかないとPUTできないようにConditionを設定したバケットポリシーを以下に示す.もっと制限をかけたければPrincipalCondtionを適宜追加する.

{
    "Version": "2012-10-17",
    "Id": "PolicyId2",
    "Statement": [
        {
            "Sid": "Test",
            "Effect": "Allow",
            "Principal": "*",
            "Action": [
                "s3:PutObject",
                "s3:PutObjectAcl"
            ],
            "Resource": "arn:aws:s3:::test-bucket/*",
            "Condition": {
                "StringEquals": "s3:x-amz-acl": "bucket-owner-full-control"
            }
        }
    ]
}

EC2の設定

EC2では,S3からオブジェクトを取得するために,今回はAWSコマンドを使う.AWSコマンドがない場合はインストールを行う.また,LambdaでSSMを使うのでEC2インスタンスにSSMエージェントが入っていない場合もインストールしておく.さらに,SSMがEC2を扱うことができるようにAmazonEC2RoleforSSMポリシーが付与されたロールをEC2にアタッチしておく.

Lambdaの設定

Lambdaでは,SSMを使ってEC2上でAWSコマンドを実行してS3からオブジェクトをEC2上に配置する.まずは,LambdaがSSMを扱うことができるようにAmazonSSMFullAccessポリシーが付与されたロールをLambdaにアタッチする.また,EC2上で実行するAWSコマンドで必要なシークレットIDとシークレットキーは環境変数としてLambdaで渡すようにする.Pythonで記述したLambdaの例を以下に示す.

import boto3
import os

ACCESS_KEY_ID = os.environ['ACCESS_KEY_ID']
SECRET_ACCESS_KEY = os.environ['SECRET_ACCESS_KEY']

ssm = boto3.client('ssm')

def lambda_handler(event, context):
    bucket = 'test-bukect/'
    ec2_dir = '/var/tmp'
    s3_dir = 'test/'
    
    commands = [
        "export AWS_ACCESS_KEY_ID=" + ACCESS_KEY_ID,
        "export AWS_SECRET_ACCESS_KEY=" + SECRET_ACCESS_KEY,
        "export AWS_DEFAULT_REGION=ap-northeast-1",
        'aws s3 sync s3://' + bucket + s3_dir + " " + ec2_dir
    ]
    ssm.send_command(
            InstanceIds = ('i-123456789',),
            DocumentName = "AWS-RunShellScript",
            Parameters = {
                "commands": commands,
                "executionTimeout": ["3600"]
            }
    )

これで,Lambdaを実行すればtest-bucketの/testディレクトリ以下にある内容がEC2上の/var/tmp以下にコピーされる.

まとめ

  • LambdaとSSMを用いてEC2からS3のオブジェクト取得させた
  • 他アカウントからのS3へのPUTではbucket-owner-full-controlをつける