はじめに
Webアプリケーションを作る際にユーザ認証を実装したいことがあります。
実装方法は様々ありますが今回は、buzzfeed/sso というプロダクトを紹介し、minikubeで作ったクラスタ上で動作させる方法を紹介します。
KubeWeekly読書メモの宣伝
私はここ半年ほどKubeWeeklyの読書メモをQiitaに公開してきました。【見てね! https://qiita.com/inajob 】
そのなかで、buzzfeed/ssoの存在を知り、さらに Single Sign-On for Internal Apps in Kubernetes using Google Oauth / SSO のようなよくできたチュートリアルも知ることができました。
この記事では上記のチュートリアルを元にminikubeで作ったクラスタ上でbuzzfeed/ssoを動作させる方法を紹介します。
buzzfeed/sso とは
buzzfeed/sso
buzzfeed/sso | |
sso, aka S.S.Octopus, aka octoboi, is a single sign-on solution for securing internal services - buzzfeed/sso |
buzzfeed/ssoはGoogleをOAuth2プロバイダとして使い、ログインを実現するための認証プロキシです。
認証プロキシは文字通り、アプリケーションの前段にプロキシを用意し、認証されていないユーザがきた場合は、認証画面に遷移されます。
認証されたユーザはアプリケーションにアクセスすることができます。
一度認証されたユーザはCookieにその情報が記録されるため、Cookieが有効な間は再認証の必要はありません。
もちろん認証プロキシをつかわずアプリケーションそのものに認証の仕組みを用意することもできますが、比較的バグが許されないロジックであり、アプリケーション各々が認証機能を実装すると、労力がかかるばかりか、バグを作り込んでしまい、セキュリティリスクを追ってしまう可能性もあります。
認証プロキシを使うことで、アプリケーションは本来の機能のみを実装し、認証の具体的なロジックを認証プロキシに任せることができます。
よく似たものとしてbitly/oauth2_proxyがありましたが、構造上スケールが難しい、ということでbuzzfeedが作り始めたのがbuzzfeed/ssoです。
Minikubeのセットアップ
今回はデモとしてMinikube上でssoを動かしてみます。
ingressを利用するのでaddonのingressを有効にします。
$ minikube start --kubernetes-version=v1.11.5
.. 結構待つ
$ minikube addons enable ingress
$ minikube ip # IPアドレスの確認
192.168.99.100
minikubeのIPアドレスはこの場合192.168.99.100です。
今回はingressのドメインとして xip.io: wildcard DNS for everyoneを利用します。
これは "好きなサブドメイン".IPアドレス.xip.io
のドメインを提供し、その結果としてURLに埋め込んだIPアドレスを返すDNSサービスです。
つまり今回の場合は hogehoge.192.168.99.100.xip.io
などという名前でクラスタ内のアプリケーションにアクセスできるようにするということです。
Google Providerのセットアップ
次にGCPで認証プロバイダのセットアップを行います。
GCPでの認証プロバイッダの利用は無料で行うことができます。
https://console.developers.google.com/cloud-resource-manager まずはこの画面から プロジェクトを作成します。
プロジェクトの作成には数分かかります。
次にOAuthのクレデンシャルを作成します。
https://console.cloud.google.com/apis/credentials/consent ここから同意画面の設定を行います。
"同意を求めるアプリの名前"というところがユーザに同意を求めるときに出てくるアプリケーション名です。
また”承認済みドメイン”に"xio.io"を追加しておきます。
次にクレデンシャルを発行します。https://console.cloud.google.com/apis/credentials の画面から行います。
”認証情報を作成”をクリックし、”OAuth クライアント ID”を選択します。
ウィザードが始まるので、下記項目を記入します。
- アプリケーションの種類: Webアプリケーション
- 名前: (なんでも良いですが) Dev
- 承認済みの JavaScript 生成元: そのまま空白で
- 承認済みのリダイレクト URI: http://sso-auth.192.168.99.100.xip.io/oauth2/callback (192...のところはminikubeのIPアドレスによって変更します。)
全てを入力して、”作成”をクリックすることで"クライアントID"と"クライアントシークレット"が作成できます。
あとで使うので、どこかに保存しておきます。
kubernetesのmanifestの用意
ここからminikube上のKubernetesクラスタにssoのデモをデプロイする手法を説明していきます。
認証後にアクセスさせるアプリケーション
認証後にアクセスさせるアプリケーションとしてhttpbinをデプロイしておきます。
これはHTTPリクエストを解析するツールで、リクエストヘッダに何がついていたかなどを見ることができます。
apiVersion: v1
kind: Service
metadata:
name: httpbin
spec:
ports:
- port: 80
protocol: TCP
targetPort: 80
selector:
run: httpbin
---
apiVersion: apps/v1beta1
kind: Deployment
metadata:
labels:
run: httpbin
name: httpbin
spec:
replicas: 1
selector:
matchLabels:
run: httpbin
template:
metadata:
labels:
run: httpbin
spec:
containers:
- image: kennethreitz/httpbin
name: httpbin
ports:
- containerPort: 80
sso Namespaceの作成
ssoがらみのコンポーネントは名前空間を分ける事にします。
$ kubectl create namespace sso
secretの作成
cookieの暗号化に使う鍵などを用意します。
内容は適当な乱数です。
kubectl create secret generic -n sso proxy-client-id --from-literal=proxy-client-id=$(openssl rand -base64 32 | head -c 32 | base64)
kubectl create secret generic -n sso proxy-client-secret --from-literal=proxy-client-secret=$(openssl rand -base64 32 | head -c 32 | base64)
kubectl create secret generic -n sso auth-code-secret --from-literal=auth-code-secret=$(openssl rand -base64 32 | head -c 32 | base64)
kubectl create secret generic -n sso proxy-auth-code-secret --from-literal=proxy-auth-code-secret=$(openssl rand -base64 32 | head -c 32 | base64)
kubectl create secret generic -n sso auth-cookie-secret --from-literal=auth-cookie-secret=$(openssl rand -base64 32 | head -c 32 | base64)
kubectl create secret generic -n sso proxy-cookie-secret --from-literal=proxy-cookie-secret=$(openssl rand -base64 32 | head -c 32 | base64)
また、Google Providerから発行された認証情報もSecretとしてデプロイしておきます
kubectl create secret generic -n sso google-client-id --from-literal=client-id=XXXXXXX
kubectl create secret generic -n sso google-client-secret --from-literal=client-secret=XXXXXXXXX
sso-auth
実際の認証を移譲するサービスです。cookieを確認し、未ログインの場合は Google OAuth2プロバイダにリダイレクトし、認証結果を受け取ります。
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: sso-auth
labels:
k8s-app: sso-auth
namespace: sso
spec:
replicas: 1
template:
metadata:
labels:
k8s-app: sso-auth
spec:
containers:
- image: buzzfeed/sso-dev:latest
name: sso-auth
command: ["/bin/sso-auth"]
ports:
- containerPort: 4180
env:
- name: SSO_EMAIL_DOMAIN # *に設定できるようだ
value: '*'
- name: HOST # sso-authを配信しているDNS名
value: sso-auth.192.168.99.100.xip.io
- name: REDIRECT_URL # OAuth2プロバイダに伝えるリダイレクト先
value: http://sso-auth.192.168.99.100.xip.io
- name: PROXY_ROOT_DOMAIN # リダイレクト先として許容するドメイン
value: 192.168.99.100.xip.io
- name: CLIENT_ID
valueFrom:
secretKeyRef:
name: google-client-id
key: client-id
- name: CLIENT_SECRET
valueFrom:
secretKeyRef:
name: google-client-secret
key: client-secret
- name: PROXY_CLIENT_ID
valueFrom:
secretKeyRef:
name: proxy-client-id
key: proxy-client-id
- name: PROXY_CLIENT_SECRET
valueFrom:
secretKeyRef:
name: proxy-client-secret
key: proxy-client-secret
- name: AUTH_CODE_SECRET
valueFrom:
secretKeyRef:
name: auth-code-secret
key: auth-code-secret
- name: COOKIE_SECRET
valueFrom:
secretKeyRef:
name: auth-cookie-secret
key: auth-cookie-secret
# STATSD_HOST and STATSD_PORT must be defined or the app wont launch, they dont need to be a real host / port
- name: STATSD_HOST
value: localhost
- name: STATSD_PORT
value: "11111"
- name: COOKIE_SECURE
value: "false"
readinessProbe:
httpGet:
path: /ping
port: 4180
scheme: HTTP
livenessProbe:
httpGet:
path: /ping
port: 4180
scheme: HTTP
initialDelaySeconds: 10
timeoutSeconds: 1
resources:
limits:
memory: "256Mi"
cpu: "200m"
---
apiVersion: v1
kind: Service
metadata:
name: sso-auth
namespace: sso
labels:
k8s-app: sso-auth
spec:
ports:
- port: 80
targetPort: 4180
name: http
selector:
k8s-app: sso-auth
upstream-config
sso-proxyの設定ファイルであるupstream-configを記述します。
これはsso-proxyに来たホスト名を見て、どのサービスにアクセスするかを振り分けるための設定です。
apiVersion: v1
kind: ConfigMap
metadata:
name: upstream-configs
namespace: sso
data:
upstream_configs.yml: |-
- service: app1
default:
from: hello.192.168.99.100.xip.io # この名前が来たら
to: http://httpbin.default.svc.cluster.local # この名前にプロキシ!
sso-proxy
サービスの前段となるプロキシのサービスです。
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: sso-proxy
labels:
k8s-app: sso-proxy
namespace: sso
spec:
replicas: 1
template:
metadata:
labels:
k8s-app: sso-proxy
spec:
containers:
- image: buzzfeed/sso-dev:latest
name: sso-proxy
command: ["/bin/sso-proxy"]
ports:
- containerPort: 4180
env:
- name: EMAIL_DOMAIN # *が指定できる
value: '*'
- name: UPSTREAM_CONFIGS # upstreamの設定
value: /sso/upstream_configs.yml
- name: PROVIDER_URL # sso-authに外から繋ぐための名前
value: http://sso-auth.192.168.99.100.xip.io
- name: PROVIDER_URL_INTERNAL # sso-authに内側からアクセスするための名前
value: http://sso-auth.sso.svc.cluster.local
- name: CLIENT_ID
valueFrom:
secretKeyRef:
name: proxy-client-id
key: proxy-client-id
- name: CLIENT_SECRET
valueFrom:
secretKeyRef:
name: proxy-client-secret
key: proxy-client-secret
- name: AUTH_CODE_SECRET
valueFrom:
secretKeyRef:
name: proxy-auth-code-secret
key: proxy-auth-code-secret
- name: COOKIE_SECRET
valueFrom:
secretKeyRef:
name: proxy-cookie-secret
key: proxy-cookie-secret
# STATSD_HOST and STATSD_PORT must be defined or the app wont launch, they dont need to be a real host / port, but they do need to be defined.
- name: STATSD_HOST
value: localhost
- name: STATSD_PORT
value: "11111"
- name: COOKIE_SECURE
value: "false"
readinessProbe:
httpGet:
path: /ping
port: 4180
scheme: HTTP
livenessProbe:
httpGet:
path: /ping
port: 4180
scheme: HTTP
initialDelaySeconds: 10
timeoutSeconds: 1
resources:
limits:
memory: "256Mi"
cpu: "200m"
volumeMounts:
- name: upstream-configs
mountPath: /sso
volumes:
- name: upstream-configs
configMap:
name: upstream-configs
---
apiVersion: v1
kind: Service
metadata:
name: sso-proxy
namespace: sso
labels:
k8s-app: sso-proxy
spec:
ports:
- port: 80
targetPort: 4180
name: http
selector:
k8s-app: sso-proxy
Ingress
sso-auth, sso-proxyをクラスタの外からアクセスできるようにするためのIngressを設定します。本当はSSLにした方が良いのですが、今回は簡単のためHTTPで設定しています。
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: sso
namespace: sso
spec:
rules:
- host: sso-auth.192.168.99.100.xip.io # sso-auth のためのルーティング
http:
paths:
- path: /
backend:
serviceName: sso-auth
servicePort: 80
- host: hello.192.168.99.100.xip.io # サービスのためのルーティング
http:
paths:
- path: /
backend:
serviceName: sso-proxy # まずはsso-proxyに飛ばす (そのあとsso-proxyが対象サービスにプロキシする。)
servicePort: 80
アプリケーションの動作確認
まずはブラウザからhttp://hello.192.168.99.100.xip.io/
にアクセスします
次のようにログインを促す画面が表示されます。
httpbinにはリクエストヘッダを見る機能があるので http://hello.192.168.99.100.xip.io/headers へアクセスしてみましょう
ヘッダを介してログインしたe-mailアドレスやユーザ名が確認できます。
まとめ
buzzfeed/ssoを利用することでアプリケーションに手を加えることなく認証機能を追加することができました。
今回の例では本当にGoogleが認証した人は誰でも通してしまう設定ですが、GCPのOrganizationと紐づけてログインユーザを管理することもできるようです。
マイクロサービスの流行により、いままでWebアプリケーションフレームワークのミドルウェアが行なっていた処理が独立したサービスとして提供されるようになって来たように感じます。
今回紹介したbuzzfeed/ssoもその典型的な例で、"認証"という機能をマイクロサービス化したものと言えます。
このようなアプリケーションにより今まで個別に実装していたビジネスロジックとは無関係の処理を任すことができるようになると、アプリケーション開発者は本来のビジネスロジックに注力することができるようになり、より効率的な開発ができるようになると感じました。
このエントリは、弊社 Z Lab のメンバーによる Z Lab Advent Calendar 2018 の7日目として業務時間中に書きました。8日目は @shmurata の担当です。
参考
- buzzfeed/ssoのQuickStart
- Google Providerの設定方法
- よくできたチュートリアル