ちょー手軽にトークン認証付きAPIへのプロキシをnginx+luaで作成する

# なにをした？

トークン認証が必要なAPIを簡単に利用するために、プロキシ用のエンドポイントをnginx+Luaで作成した。（実際に利用したのはOpenRestyですが）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/334782/68500f68-e840-7c66-e082-1c59090ae7ec.png)

といっても、それまでクライアントでやってた「トークン取得」と「トークンをつけたリクエスト」をサーバ上で肩代わりさせただけです。


# なぜやろうとしたか？

以前に **[PhotonOSでk8sクラスタ作成](https://qiita.com/bashaway/items/4c3c173f6dbc3193d72a)** したとき、dockerのratelimitに引っかかったかどうか、以下のように確認していました。

```shell-session
$ TOKEN=$(curl -sSL "https://auth.docker.io/token?service=registry.docker.io&scope=repository:ratelimitpreview/test:pull" | jq -r .token)
$  curl -i -sSL -v -H "Authorization: Bearer $TOKEN" https://registry-1.docker.io/v2/ratelimitpreview/test/manifests/latest 2>/dev/null | egrep "ratelimit-|rate limit"
ratelimit-limit: 100;w=21600
ratelimit-remaining: 65;w=21600
docker-ratelimit-source: x.x.x.x(← これはアクセス元のIPアドレスになる）
```


ただ、コマンドラインで２行分ペーストするのは面倒だし、戻ってくる情報もJSONじゃないし（そもそもJSONを返すオプションがあるのか？とかを調べるのが面倒でもあった）、なんかスマートじゃないなぁ。と思った。



# で、どうなった？

最終的に、内部からのみアクセスできるWebサーバの特定のエンドポイントにアクセスすると、必要な情報だけ取得できるようになりました。

```shell
$ curl -s http://[自サーバ]/docker-limit/ | jq
{
  "limit": "100;w=21600",
  "remain": "61;w=21600",
  "source": "x.x.x.x"
}
```

# 検証環境

* CentOS7( CentOS Linux release 7.9.2009 (Core) )
* OpenResty( nginx version: openresty/1.21.4.1 )



# 作業内容

## OpenResty導入

```shell
sudo yum-config-manager --add-repo https://openresty.org/package/centos/openresty.repo
sudo yum install -y openresty
```

## OpenResty設定

トークン取得して、プロキシ用エンドポイントへのリクエストがあった場合に転送する設定を書きます。
とりあえず動作する最低限の設定のみなので、これ１ファイルで全文です。

dockerじゃないサービスの場合は、以下の変更が必要です
* トークン取得用のlocationにある proxy_pass のプロキシ先
* ngx.req.set_header でセットするヘッダ名と、対象のトークンがレスポンスのどのキーに含まれているか
* APIプロキシ用のlocationにある proxy_pass のプロキシ先


```nginx:/usr/local/openresty/nginx/conf/nginx.conf
events {
  worker_connections  1024;
}

http {
    include       mime.types;
    default_type  application/octet-stream;

    server {
        listen       80;
        server_name  _;

        # トークン取得用の location
        # （「このサーバから取得したトークン」が取得できてしまうので、internalで設定する）
        location /get-docker-token/ {
          internal;
          proxy_pass https://auth.docker.io/token?service=registry.docker.io&scope=repository:ratelimitpreview/test:pull;
        }

        # APIプロキシ用の location
        location ~ ^/docker-proxy/ {
          access_by_lua_block {
            -- json処理するためのモジュールを読み込み
            local cjson = require "cjson"


            -- DockerAPIにアクセスするためのトークンをリクエスト
            local res = ngx.location.capture("/get-docker-token/")

            -- トークンが取得できていたらリクエストヘッダにセット
            if res.status == ngx.HTTP_OK then
              body = cjson.decode(res.body)
              -- 認証ヘッダは Authorization で、値はレスポンスJSONのtokenキーの値
              ngx.req.set_header("Authorization", "Bearer "..body.token)
            else
              ngx.log(ngx.ERR, 'GET TOKEN REQUEST: FAILED')
              ngx.exit(500)
            end
          }

          # もとのリクエストパスをプロキシのパスにする
          rewrite ^/docker-proxy/(.*)$ /$1 break;

          # lua内で付与した Authorization をそのまま利用するため、proxy_pass_header する
          proxy_pass_header Authorization;

          # docker-proxyでアクセスさせるAPIドメインを設定
          proxy_pass https://registry-1.docker.io;
        }
    }
}
```


アクセスしてみる

```shell:/docker-proxy/の例
$ curl -i -s http://[自サーバ]/docker-proxy/v2/ratelimitpreview/test/manifests/latest | egrep "ratelimit-|rate limit"
ratelimit-limit: 100;w=21600
ratelimit-remaining: 66;w=21600
docker-ratelimit-source: x.x.x.x
```

汎用的なAPIプロキシが完成！

でも、スマートさに欠けるなぁ


# 整形したい

汎用的なAPIプロキシ用のエンドポイントを作成しても、結局使うのはratelimitの部分だけなので、ここに対するプロキシに限定して、レスポンスを整形してから返してあげることにした。

```nginx:/usr/local/openresty/nginx/conf/nginx.conf
events {
  worker_connections  1024;
}

http {
    include       mime.types;
    default_type  application/octet-stream;

    server {
        listen       80;
        server_name  _;

        # トークン取得用の location
        # （「このサーバから取得したトークン」が取得できてしまうので、internalで設定する）
        location /get-docker-token/ {
          internal;
          proxy_pass https://auth.docker.io/token?service=registry.docker.io&scope=repository:ratelimitpreview/test:pull;
        }

        # ratelimitを取得するための location
        # lua内で付与した Authorization をそのまま利用するため、proxy_pass_header して使う
        location /get-docker-limit/ {
          proxy_pass_header Authorization;
          proxy_pass https://registry-1.docker.io/v2/ratelimitpreview/test/manifests/latest;
        }


        # ratelimit専用のAPIプロキシ用の location
        location /docker-limit/ {
          access_by_lua_block {
            -- json処理するためのモジュールを読み込み
            local cjson = require "cjson"


            -- DockerAPIにアクセスするためのトークンをリクエスト
            local res = ngx.location.capture("/get-docker-token/")

            -- トークンが取得できていたらリクエストヘッダにセット
            if res.status == ngx.HTTP_OK then
              body = cjson.decode(res.body)
              ngx.req.set_header("Authorization", "Bearer "..body.token)
            else
              ngx.log(ngx.ERR, 'GET TOKEN REQUEST: FAILED')
              ngx.exit(500)
            end


            -- DockerAPIへratelimitを問い合わせ
            res = ngx.location.capture("/get-docker-limit/")

            -- 問い合わせ結果が返ってきたら、レスポンスヘッダのratelimit関連を整形して出力
            if res.status == ngx.HTTP_OK then
              --body = cjson.decode(res.body)
              result = {
                limit  = res.header["ratelimit-limit"],
                remain = res.header["ratelimit-remaining"],
                source = res.header["docker-ratelimit-source"]
              }
              ngx.say(cjson.encode(result))
            else
              ngx.log(ngx.ERR, 'GET LIMIT REQUEST: FAILED')
              ngx.exit(500)
            end
          }
        }
    }
}
```

（luaモジュールが入るとシンタックスがうまくいかないなぁ）

アクセスしてみる

```shell
$ curl -s http://[自サーバ]/docker-limit/ | jq
{
  "limit": "100;w=21600",
  "remain": "61;w=21600",
  "source": "x.x.x.x"
}
```

やったぜ（今日はのこり６１回分だ！）


# さいごに

ngx.location.capture には直接URLを指定できないがためにlocationを設定しているだけなので、これらをinternalにするのはもちろんですが、プロキシ用エンドポイント自体も外部にさらさないようにしておく必要があります。そうしないと、外部の人が認証トークンを使ってアクセスできてしまうので、まじぃです。

プロキシ用のエンドポイントを利用するための認可をつけておくと良いですね。


# 参考

https://github.com/openresty/lua-nginx-module