リモートワークでも便利な認証付きプロキシを作っている

7 min読了の目安(約6600字TECH技術記事

リモートワークでも重宝する認証付きプロキシをずっと自作しているので、それをご紹介します。

認証付きプロキシとは

リバースプロキシとして振る舞い、リクエストが誰から行われたのか検証してからバックエンドへ転送するプロキシです。

これによりインターネットから直接プロキシにアクセスできるようにすることができます。
したがってVPNを使わずに社内ツールへのアクセスを可能にすることができます。

最近ではこういったプロキシは ゼロトラストプロキシ と呼ばれます。
しかしプロキシに認証機能を付加するというアイデアは昔からあり、様々な実装があります。

例えば nginx の ngx_http_auth_request_module もそのうちのひとつです。
他にも pomeriumoathkeeper といったプロジェクトもあります。

なぜ作っているのか

元々はリモートワークのためではなく、自宅にあるk8sクラスタで動いているサービスにインターネットからアクセス出来るようにするためでした。
例えば Grafana や Prometheus などを自宅以外から見たくなることがあります。そういった場合にインターネットからでも安全に内部ツールを閲覧出来る方法が欲しかったのです。

インターネットからアクセスできるようにするためには全通信を暗号化したいです。
そしてそれは例外なく適用したい。絶対に全てのトラフィックが暗号化されるようにしたいところです。

それを実現するためプロキシを自作しそのプロキシを通るようにしているので必ずHTTPS通信になります。

また自宅のネットワークであるという特殊事情も自作している理由の一つです。

通常自宅のインターネットではグローバルIPが一つしか付与されません。つまり、この一つのIPアドレスを使って様々なサービスへのインターネットからのReachabilityを確保する必要があります。仕事であれば複数のグローバルIPが使える環境を用意するのは簡単ですが自宅ですとコストの問題があり、それは容易ではありません。
そのため、付与された一つのIPアドレスを有効活用するため自作する必要がありました。

例えばHTTPSだけではなく複数のホストへのSSHなどもプロキシすることができます。
SSHのトラフィックをプロキシする場合でも、HTTPSの場合と同様に認証を行います。(ssh over TLSとなっている)
これにより自宅ネットワークからでもインターネットからでもシームレスに全く同じコマンドでSSH接続をすることができます。

また、ただ高機能なプロキシを実装するだけではなく、人事DBと連携することで入社から権限付与までをシームレスに行えたり端末に内部PKIがサインした証明書を配るようにするなど、ただのリバースプロキシにとどまらない、時代に即したセキュリティツールの実験的実装という側面もあります。

特徴

次の図がプロキシの動作を大まかに示したものです。

ブラウザは最初にプロキシへアクセスします。

しかしこの時点では認証が行われていないのでプロキシはIDプロバイダーへブラウザを誘導し認証するように促します。
IDプロバイダーでアクセスした本人が認証し、その情報を持って再度プロキシへアクセスします。プロキシはブラウザから受け取った情報の検証とバックエンドへのアクセス権限の検証後、バックエンドへリクエストを転送します。

OpenID Connect

ユーザーの認証は外部に移譲しています。

プロキシ自体がユーザーの認証まで担当すると、IDとパスワードの管理が必要になってしまうこと、そのためにユーザーは新たなパスワードを覚えなければいけなくなってしまうためです。

SSO(Single sign-on)をOpenID Connectで実現しIdP(Identity Provider)を自ら持たないようにしています。

プロキシの設定でOpenID Connectを使わない設定ができるわけではなく、 必ずOpenID Connectを使う必要があります

OpenID Connectを提供しているIdPは多くあり、Google(Google Workspaceも含む)・Okta・AzureなどはOpenID Connectを提供しています。

L7とL4

主にHTTPのトラフィックをプロキシすることを目的に実装されていますが、それだけではなくL4のプロキシとしても振る舞うことができます。

前述のSSHのプロキシはL4プロキシとして機能させ実現しています。

L4プロキシの場合は、認証を行うために中継するコマンドが存在します。

SSHであれば次のような設定ファイルにより、簡単にプロキシ経由のアクセスにすることができます。

Host *.internal.f110.dev
    ProxyCommand heim-tunnel proxy %

該当のホストへ接続しようとすると、まずブラウザが開きます。
このページはプロキシが提供しており、そこでその接続が本当にユーザーによって行われたものなのか認証を行います。

アクセスが許可されれば自動的に接続が行われます。

SSHに二要素認証を入れている場合は似たようなフローになります。
(二要素認証の場合は先に公開鍵などでの認証を行ってから二要素認証になりますが、プロキシは接続する前に認証を行う点は異なります)

このコマンドは標準入力と出力がバックエンドの出力と入力に繋がります。
したがってSSHだけではなく、同様の仕組みで中継できるものであればプロキシすることができます。

ユーザー管理

自宅で利用する場合はユーザーが非常に少数なので設定ファイルで直接指定ということも可能ではありますが、本プロキシはユーザー管理画面を実装しています。

これがユーザーの管理画面です。

これは会社のような大きなユーザー規模で利用することも想定しているからです。

ユーザーの権限管理はRBACで実装されています。

組織の場合、各ユーザーが「どの部署・チームに所属しているか」で権限管理を行うのではなく「何をする人なのか」で管理を行った方が管理しやすいと考えています。
またこの管理は特定の管理者が一括して行うのではなく、様々な人へ移譲できるようになっているべきです。

こうした考えのもと、RBACで実装されており「何をする人」=ロールを個人へ付与する形になっています。

各ロールごとにそれを管理する人を指定することが出来ます。その人は自分が管理できるロールへユーザーを追加したり削除したりすることができます。

このように権限管理を移譲することでプロキシ本体の管理者に負荷が集中することなく大きな組織でも少数の管理者で運用できるようになります。

NAT超え

クライアント(ブラウザ)がNAT超えをできるのは当然ですが、本プロキシはバックエンドもNAT超えをすることができます。
nginx でも他のツールと組み合わせるとなんとか実現可能ではありますが、本プロキシではそれ自体の機能として実装されているため、安全な仕組みになっています。

例えばプロキシをAWS上で構築しておき、オフィスに設置したJenkinsへ 特別なルーティングを追加することなく プロキシすることができます。

バックエンド先と対になるように専用のコマンドを起動しておくとそれを通してプロキシすることができます。

専用のコマンドはまずプロキシに対して接続し、トラフィックが来るのを待機します。
プロキシはその接続を利用してリクエストを転送します。

下の図はバックエンドが別ネットワークに存在する場合の概略図です。
矢印の方向が接続の方向です。コネクターからプロキシへ向かって矢印が伸びている点が通常のプロキシとは大きく異なります。

組織だと色々な部署が独自にサービスを構築していたり、イントラとサービスは明確に別のネットワークにあり疎通しないということは普通かと思いますが、そういった場合でも一つのプロキシさえあれば安全にアクセスすることが出来ます。

自宅クラスタではこの機能を利用し、各端末へプロキシ経由でSSHできるようにしています。
自宅のネットワークはクラスタのセグメントとクライアントのセグメントが分離されていて、クラスタ側からクライアント側へポート22の接続は出来ないようになっています。
プロキシからクライアントへ直接接続できないためクライアント側で専用のコマンドを起動しておきプロキシできるようにしています。

デプロイツール

プロキシの本体だけではなく、デプロイツールも実装しています。

前述の通り自宅のk8sクラスタにデプロイすることを目的として設計・実装されているため、デプロイツールは kubernetes controller として実装されており、コントローラを動作させていれば次のようなオブジェクトを作成するだけでプロキシがセットアップされます。
(実際にはもっと様々な設定が必要なため spec 部分に必須パラメータがいくつもあります)

apiVersion: proxy.f110.dev/v1alpha2
kind: Proxy
metadata:
  name: internal
spec:
  domain: internal.f110.dev

プロキシは etcd をデータストアとして利用していますが、etcdもクラスタ上に構築します。

etcd のオートヒールも実装しているのである程度の障害までは耐えることが出来ます。
バックアップ・レストア機能もあるので、 quorum を満たせなくなる障害が発生した場合でもバックアップからレストアし etcd クラスタを復旧させることができます。(当然ながらこの場合はデータがロールバックします)

さらにデプロイツール自身のデプロイも簡単に行えるようになっています。

適用するだけでデプロイできるマニフェストを用意しているので次のコマンドで controller を動かすことができます。

$ kubectl apply -f https://github.com/f110/heimdallr/releases/latest/download/all-in-one.yaml

apply後少し待つと heimdallr というネームスペースに controller の Pod が一つだけ動いているはずです。

(実際に利用するのであればこのように apply せず何らかのCDツールを使って apply することをおすすめします。自宅クラスタでは ArgoCD を使って apply しています)

リモートワークで使って欲しい理由

多くの場合、リモートワークではVPNを使っているのではないでしょうか。

しかしVPNは一箇所にトラフィックが集まってしまうためそこがボトルネックになりやすく、全社で一斉にリモートワークへ移行した際VPNが詰まってしまい仕事にならないといったことが起こりやすいです。

本プロキシはスケールするようにも設計されているため、トラフィックが増えた場合はプロセスを増やすだけで対応出来ます。1プロセスでも性能はそこそこあるため、冗長構成を取るだけでイントラ向けプロキシとしては十分な性能が出ます。

自宅クラスタでも重宝

前述した通り、自分が自宅の k8s クラスタで使うために設計・実装しています。
したがって自宅にある k8s クラスタでも重宝します。

自宅クラスタの場合は色々なサービスを自分で用意する必要がありますが、こういった機能を提供してくれるプロキシはあまりありません。
更に k8s とインテグレートされているものはほとんどないでしょう。
k8s とインテグレートされているというのは、プロキシ本体のデプロイ・設定がマニフェストで行えるだけではなく、バックエンドの設定も Service を参照する形で行えるということです。

apiVersion: proxy.f110.dev/v1alpha2
kind: Backend
metadata:
  name: unifi
  namespace: unifi
  labels:
    instance: internal
spec:
  layer: tools
  http:
    - path: /
      insecure: true
      serviceSelector:
        namespace: unifi
        scheme: https
        port: https-gui
        matchLabels:
          app.kubernetes.io/instance: unifi-controller
          role: gui
  permissions:
    - name: all
      locations:
        - any: /

このようにバックエンドの設定は直接アドレスを記述するのではなく Service をセレクタを使って参照します。
これによりポータビリティも向上しますし、提供されているマニフェスト(helm chartなど)とのインテグレーションも容易になります。

controller を使ってデプロイしていれば全ての設定がマニフェストで行え、また限定的ではありますが Ingress もサポートしています。
マニフェストで全ての設定が行えるとクラスタ全体の GitOps で統一的に設定を行うことができます。

自宅だとグローバルIPが非常に限られていると思うので、一つのプロキシで内部ツールと公開サービスのトラフィックを捌きたいという場合もあるでしょう。
そんな場合でも本プロキシは対応することができます。

まだ開発中

実際に利用していく分には十分な機能があります。しかし、まだサポートしたいユースケースを全てサポートできているわけではないので開発は続いています。

他にも内部でCAを持っていますが、これは人によっては外部のCAを使いたいといったケースがあるかもしれません。なので、外部のCAをサポートしたいとも思っています。
(内部CAを中間CAにして運用するという手もありますが、中間CAにもしたくなく証明書の管理を外部CAの外に出したくない場合というのを考えています)

参考