コンテンツにスキップ

SecurityGroupでPodの通信を制限する

Juneauでは、SecurityGroupを使ってPod単位でステートフルな許可ルールを記述し、不要な通信を遮断できます。 このガイドでは、最小構成のWebアプリケーションを題材に、

  1. backend Podへの受信を許可するクライアントだけを絞る
  2. それ以外のPodからは到達できないことを確認する
  3. SecurityGroup同士の参照で「同じロールを持つPod群」をまとめて許可する

までの一連の手順を示します。

このガイドで構築するもの

  • 専用Vpc (app-vpc) と Subnet 2つ
    • app-subnet (10.80.0.0/24): backend Pod配置先
    • client-subnet (10.80.1.0/24): クライアントPod配置先
  • backend用のSecurityGroup (web-sg)
  • 許可されたクライアント用のSecurityGroup (client-sg)
  • backend Pod (nginx) にweb-sgを付与
  • 許可されたクライアントPodにclient-sgを付与
  • client-sgを付与したクライアントからbackendへの通信が成立し、付与していないクライアントからは遮断されること

前提条件

  • Juneauのcontroller/daemonが動作しているクラスター
  • kubectlが利用可能なこと

手順

1. Vpcと2つのSubnetを作成

apiVersion: juneau.loutres.me/v1alpha1
kind: Vpc
metadata:
  name: app-vpc
---
apiVersion: juneau.loutres.me/v1alpha1
kind: Subnet
metadata:
  name: app-subnet
spec:
  vpc: app-vpc
  cidr: 10.80.0.0/24
---
apiVersion: juneau.loutres.me/v1alpha1
kind: Subnet
metadata:
  name: client-subnet
spec:
  vpc: app-vpc
  cidr: 10.80.1.0/24

詳細はVpc / Subnetを参照してください。

2. クライアント側のSecurityGroupを作成

backendへの到達を許可したいクライアントに付ける目印として、ルールを持たない空のSecurityGroupを作ります。

apiVersion: juneau.loutres.me/v1alpha1
kind: SecurityGroup
metadata:
  name: client-sg
spec:
  vpc: app-vpc

spec.ingressを空にすると、client-sgを付けたPodは何も受信できなくなります。 このガイドではクライアント側は受信を許可する必要が無い (送信のみ) ため、空のままで問題ありません。

3. backend用のSecurityGroupを作成

client-sgを付けたPodからのTCP/80だけを受信可能にします。

apiVersion: juneau.loutres.me/v1alpha1
kind: SecurityGroup
metadata:
  name: web-sg
spec:
  vpc: app-vpc
  ingress:
    - from:
        - securityGroupRef:
            name: client-sg
      protocol: tcp
      ports:
        - port: 80

securityGroupRefに同じVpcのSecurityGroupを指定すると、そのSecurityGroupを付けた任意のPodからのトラフィックがマッチします。CIDRと違いPodのIPアドレスが入れ替わっても動作するので、Podが再作成される環境でも安定して使えます。

$ kubectl get securitygroup
NAME        VPC       GROUPID   INGRESS   EGRESS   READY
client-sg   app-vpc   1         0                  True
web-sg      app-vpc   2         1                  True

READY: TrueになればSecurityGroupは反映済みです。INGRESS列はspec.ingressを許可エントリに展開したあとの件数です。 詳細はSecurityGroupを参照してください。

4. backend Podをデプロイ

web-sgjuneau.loutres.me/security-groups annotationで付与します。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx
spec:
  replicas: 1
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
      annotations:
        juneau.loutres.me/subnet: app-subnet
        juneau.loutres.me/security-groups: web-sg
    spec:
      containers:
        - name: nginx
          image: nginx:1.27

5. 許可されたクライアントPodをデプロイ

client-sgを付けたクライアントPodを別Subnetに配置します。

apiVersion: v1
kind: Pod
metadata:
  name: curl-allowed
  annotations:
    juneau.loutres.me/subnet: client-subnet
    juneau.loutres.me/security-groups: client-sg
spec:
  containers:
    - name: curl
      image: curlimages/curl:8.7.1
      command: ["sleep", "infinity"]

6. 許可されていないクライアントPodをデプロイ

比較対象として、SecurityGroupを付けていないクライアントPodも用意します。

apiVersion: v1
kind: Pod
metadata:
  name: curl-denied
  annotations:
    juneau.loutres.me/subnet: client-subnet
spec:
  containers:
    - name: curl
      image: curlimages/curl:8.7.1
      command: ["sleep", "infinity"]

7. 通信を確認

backend PodのIPアドレスを取得して、両方のクライアントから到達を試みます。

$ BACKEND=$(kubectl get pod -l app=nginx -o jsonpath='{.items[0].status.podIP}')
$ kubectl exec curl-allowed -- curl -sS --max-time 5 http://$BACKEND/
<!DOCTYPE html>
...
<h1>Welcome to nginx!</h1>

$ kubectl exec curl-denied -- curl -sS --max-time 5 http://$BACKEND/
curl: (28) Connection timed out after 5001 milliseconds

client-sgを付与したcurl-allowedからは応答が返り、付与していないcurl-deniedからは到達できません。

別のパターン: CIDRで許可する

ピアのSecurityGroupではなく、IPレンジで受信を許可したい場合はsecurityGroupRefの代わりにcidrを指定します。

apiVersion: juneau.loutres.me/v1alpha1
kind: SecurityGroup
metadata:
  name: web-sg
spec:
  vpc: app-vpc
  ingress:
    - from:
        - cidr: 10.80.1.0/24
      protocol: tcp
      ports:
        - port: 80

このルールを持つbackendは、10.80.1.0/24のいずれかのアドレスから来たTCP/80を受信します。

特定のクライアントレンジに絞れる反面、Podの再スケジューリングでアドレスが変わる前提のクラスタではsecurityGroupRefの方が扱いやすいです。両者は同じルール内に混在させることもできます。

別のパターン: 送信側を制限する

spec.egressを明示すると送信が制限されます (省略時は全許可)。 たとえば「クライアントPodはweb-sgを付けたbackendにしか送信できない」ようにするには、client-sgを次のように書き換えます。

apiVersion: juneau.loutres.me/v1alpha1
kind: SecurityGroup
metadata:
  name: client-sg
spec:
  vpc: app-vpc
  egress:
    - to:
        - securityGroupRef:
            name: web-sg
      protocol: tcp
      ports:
        - port: 80

spec.egressを一度でも書くと、明示していない宛先への送信は全て遮断されます。クラスタ内DNSや外部APIに到達したい場合は、必要なピアもルールに追加してください。

Vpcで強制する

「このVpcのPodには必ずSecurityGroupを付ける」運用を徹底したい場合、Vpcにspec.enforceSecurityGroups: trueを設定します。

apiVersion: juneau.loutres.me/v1alpha1
kind: Vpc
metadata:
  name: app-vpc
spec:
  enforceSecurityGroups: true

この設定が有効な間、Vpc配下のSubnetに作成しようとするPodでjuneau.loutres.me/security-groups annotationが空 (または無効なSecurityGroupしか含まない) 場合、Podの作成は拒否されます。 有効化はVpc単位なので、本番系Vpcのみに適用するなどの使い分けができます。

うまくいかないとき

  1. Podの作成が拒否される (enforceSecurityGroups)
    • 対象Vpcのspec.enforceSecurityGroupstrueの場合、Pod annotationで少なくとも1つの有効なSecurityGroupを付ける必要があります
    • 指定したSecurityGroupが存在し、かつPodのSubnetと同じVpcに属しているか確認してください
  2. 許可したはずのPodから到達しない
    • kubectl get securitygroup <name>READY: Trueを確認
    • status.ingressRuleCount / status.egressRuleCountが想定通りの件数になっているかを確認 (0件なら全遮断 / 全許可のデフォルト動作になります)
    • securityGroupRefで参照しているSecurityGroupとbackendの両方が同じVpcに属しているか確認
    • クライアントPodのannotationでSecurityGroupが正しく付いているかをkubectl get pod <name> -o yamlで確認
  3. 遮断したいPodからも通ってしまう
    • 許可ルールはOR評価です。複数SecurityGroupを付与した場合は、いずれか1つでも許可すれば通ります。意図せず広すぎるcidrを指定していないか確認してください
    • SecurityGroupを付与していないPodはSecurityGroupによる制限を受けません。全Podに付与を強制したい場合はspec.enforceSecurityGroups: trueを検討
  4. securityGroupRefが見つからない旨のエラー
    • 参照先のSecurityGroupがまだ作られていない、もしくは別のVpcに属しています。spec.vpcを揃えて作り直してください

参照