y-ohgi's blog

しがないSREなフリーランスの徒然

GKEでhttps

概要

GKEでhttpsを用いるために、Let's Encrypt cert-managerを試した覚え書き。

jetstack/cert-manager: Automatically provision and manage TLS certificates in Kubernetes

環境

$ kubectl version
Client Version: version.Info{Major:"1", Minor:"8", GitVersion:"v1.8.6", GitCommit:"6260bb08c46c31eea6cb538b34a9ceb3e406689c", GitTreeState:"clean", BuildDate:"2017-12-21T06:34:11Z", GoVersion:"go1.8.3", Compiler:"gc", Platform:"darwin/amd64"}
Server Version: version.Info{Major:"1", Minor:"9+", GitVersion:"v1.9.6-gke.1", GitCommit:"cb151369f60073317da686a6ce7de36abe2bda8d", GitTreeState:"clean", BuildDate:"2018-04-07T22:06:59Z", GoVersion:"go1.9.3b4", Compiler:"gc", Platform:"linux/amd64"}
$ helm version
Client: &version.Version{SemVer:"v2.9.1", GitCommit:"20adb27c7c5868466912eebdf6664e7390ebe710", GitTreeState:"clean"}
Server: &version.Version{SemVer:"v2.9.1", GitCommit:"20adb27c7c5868466912eebdf6664e7390ebe710", GitTreeState:"clean"}

証明書の自動発行プロトコルについて

Let's Encryptの証明書発行を行うためにACMEというプロトコルがあります。
これはLet's Encryptが策定したSSL/TLS証明書発行のためのプロトコルで、IETFで標準化されています。
ietf-wg-acme/acme

ACMEを用いた証明書発行方法は何パターンか手段がありますが、cert-managerではHTTPもしくはDNSを用いる方法がサポートされています。

  • 1つ目のHTTPを用いる手段(http-01 challenge)。
    • これはLet's Encryptの認証局から発行されたトークンを受け取り、 http://<YOUR DOMAIN>/.well-known/acme-challenge/ のパスに受け取ったパスを反映させ、認証局からアクセスを受けドメインを所有していることを証明します。
  • 2つ目にDNSを用いる手段(dns-01 challenge)。

今回はCloud DNSを用いてDNSでの証明を行います。

手順

GKE Clusterの作成

少しでも節約したいのでプリエンプティブインスタンスを使う以外は特に未設定

$ gcloud beta container clusters create "sample-cluster" \
    --zone "asia-northeast1-a" \
    --preemptible --num-nodes "3" \
    --cluster-version "1.9.6-gke.1"\
    --machine-type "n1-standard-1"

Helmのインストール

$ helm init だけだと権限が足りないので、cluster-adminを付与

$ brew install kubernetes-helm
$ kubectl create serviceaccount tiller --namespace kube-system
$ kubectl create clusterrolebinding tiller --clusterrole=cluster-admin --serviceaccount=kube-system:tiller
$ helm init --upgrade --service-account tiller

GCPのService Accountを作成

DNS-01 Challengeを行うためにはKubernetesからDNSを編集する必要がある。
そのためのGCPのService Accountを発行と、発行したService AccountをKubernetesへ渡す。

$ export GCLOUD_PROJECT=$(gcloud config get-value project)
$ gcloud iam service-accounts create cert-manager --display-name "cert-manager"
$ gcloud projects add-iam-policy-binding ${GCLOUD_PROJECT} --member serviceAccount:cert-manager@${GCLOUD_PROJECT}.iam.gserviceaccount.com --role roles/dns.admin
$ gcloud iam service-accounts keys create cert-manager-key.json --iam-account cert-manager@${GCLOUD_PROJECT}.iam.gserviceaccount.com
$ kubectl create secret generic clouddns-service-account --from-file=cert-manager-key.json=cert-manager-key.json -n kube-system

cert-managerのインストール

helmでサクッと

$ helm install stable/cert-manager --namespace kube-system

ClusterIssuer/Certificateの用意

Issuerには IssuerClusterIssuer の2種類あります。
Issuer は単一namespaceを対象にしているのに対して、 ClusterIssuerクラスター全体を対象にしていることが違いになります。
Issuerの役割としては「証明書の認証局についての設定」です。
// 今後Let's Encrypt以外の証明書発行業者が対応し始めたらserverのURLが変わったりするイメージなのかなーと。

ClusterIssuerを作成するために clusterissuer.yaml を以下のように記述します。
letsencrypt-stagingletsencrypt-prod はそれぞれ開発用と本番用にLet's Encryptが用意してくれている証明局になります。
Staging Environment - Let's Encrypt - Free SSL/TLS Certificates

apiVersion: certmanager.k8s.io/v1alpha1
kind: ClusterIssuer
metadata:
  name: letsencrypt
  namespace: kube-system

spec:
  acme:
    server: https://acme-v01.api.letsencrypt.org/directory
    email: '' #TODO: YOUR EMAIL ADDRESS
    privateKeySecretRef:
      name: letsencrypt
    http01: {}
    dns01:
      providers:
      - name: gcp-dns
        clouddns:
          serviceAccountSecretRef:
            name: clouddns-service-account
            key: cert-manager-key.json
          project: '' #TODO: YOUR GCP PROJECT

次にCertificateを作成します。
これは証明書自体の設定で、扱うドメインについて記述します(SANも対応)。

Certificateを作成するために certificate.yaml を以下のように記述します。

apiVersion: certmanager.k8s.io/v1alpha1
kind: Certificate
metadata:
  name: '' #TODO: certificate name

spec:
  secretName: cert-manager-tls
  issuerRef:
    name: letsencrypt
    kind: ClusterIssuer
  commonName: '' #TODO: YOUR DOMAIN
  dnsNames:
    - '' #TODO: YOUR DOMAIN
  acme:
    config:
      - dns01:
          provider: gcp-dns
        domains:
          - '' #TODO: YOUR DOMAIN

最後に記述したmanifestをそれぞれ適用させます。

$ kubectl apply -f clusterissuer.yaml -n kube-system
$ kubectl apply -f certificate.yaml

サンプルアプリの構築

雑にnginxを動かす例

Deployment/Service

apiVersion: apps/v1
kind: Deployment
metadata:
  name: web
  labels:
    app: web

spec:
  replicas: 2
  selector:
    matchLabels:
      app: web
  template:
    metadata:
      labels:
        app: web
    spec:
      containers:
        - name: nginx
          image: nginx:1.13
          ports:
            - containerPort: 80

---
kind: Service
apiVersion: v1
metadata:
  name: web-service

spec:
  type: NodePort
  selector:
    app: web
  ports:
    - protocol: TCP
      port: 80
      targetPort: 80
---

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: web-ingress

spec:
  tls:
    - secretName: cert-manager-tls
      hosts:
        - '' #TODO: YOUR DOMAIN
  backend:
    serviceName: web-service
    servicePort: 80

ドメインの設定

ingressのIPをCloud DNSへ登録。
反映にちょっと時間が必要

$ export ZONE=<YOUR CLOUD DNS ZONE>
$ export DOMAIN=<YOUR DOMAIN>
$ export INGRESS_IP=$(kubectl get ing -o jsonpath='{.items[].status.loadBalancer.ingress[].ip}')
$ export GCLOUD_PROJECT=$(gcloud config get-value project)
$ gcloud dns --project=$GCLOUD_PROJECT record-sets transaction start --zone=$ZONE
$ gcloud dns --project=$GCLOUD_PROJECT record-sets transaction add $INGRESS_IP --name=${DOMAIN}. --ttl=300 --type=A --zone=$ZONE
$ gcloud dns --project=$GCLOUD_PROJECT record-sets transaction execute --zone=$ZONE

おまけ:HSTS

Cloud LoadBalancerは / のパスで200を返す必要があるので、以下のようなnginxコンフィグで対応すると良いかもです。

/etc/nginx/nginx.conf

user  nginx;

error_log /var/log/nginx/error.log warn;
pid        /var/run/nginx.pid;

events {
    worker_connections  1024;
}

http {
    include       /etc/nginx/mime.types;
  
    default_type  application/octet-stream;
    log_format  main  '$server_name $remote_addr - $remote_user [$time_local] "$request" '
    '$status $body_bytes_sent "$http_referer" '
    '"$http_user_agent" "$http_x_forwarded_for"';
  
    access_log /var/log/nginx/access.log main;
  
    sendfile on;
    tcp_nopush on;
    tcp_nodelay on;
    keepalive_timeout 65;
  
    # Cloud LoadBalancer用ヘルスチェック
    server {
        listen 80;
        location / {
            add_header Content-Type "text/plain";
            return 200 "ok";
        }
    }
  
    include /etc/nginx/conf.d/*.conf;
}

/etc/nginx/conf.d/default.conf

server {
    server_name example.com;

    listen      80;
    
    if ($http_x_forwarded_proto != https) {
        return 301 https://$host$request_uri;
    }
    
    location / {
        root   /usr/share/nginx/html;
        index  index.html index.htm;
    }
}

参考