【Docker】【Dbeaver】Cloudbeaver Community Edition で Reverse proxy header authentication を試す

# はじめに

CloudbeaverとはWeb版のDbeaverです。Dockerイメージがあり簡単に構築できます。

https://github.com/dbeaver/cloudbeaver/

https://hub.docker.com/r/dbeaver/cloudbeaver

利用に辺り、Reverse proxy header authenticationを試します。

>CloudBeaver offers a feature for authorization and authentication using reverse proxy headers. This method allows to authenticate users via specific HTTP header fields.

https://github.com/dbeaver/cloudbeaver/wiki/Reverse-proxy-header-authentication

# Cloudbeaverの注意点等

Cloudbeaverには匿名認証が用意されていますが、この場合DBへの接続情報を永続化できないという特徴があります。

>Private connections: When enabled, this feature lets anonymous users configure their private connections from the main page. These connections are temporary and expire with the session.

このため、接続情報を永続化するには、何らかの方法でログインする必要があるようです。

また、デフォルトのままだと、独自の管理者アカウントのユーザーとパスワードの設定を行わなければならないので、既存の認証基盤との連携やSSOなどを実現したい際にやや使いづらさを感じます。

組織よっては、統一的なユーザーが用意されていて、それを利用して様々なアプリケーションへSSOすることも多いのではないでしょうか。

ただ、今回はあまり複雑なことはせず、基本認証でログインしたユーザーでCloudbeaverにログインする方法を今回は試してみます。




https://github.com/dbeaver/cloudbeaver/wiki/Anonymous-Access-Configuration#overview

# ソース

https://github.com/v1tam1nb2/openresty-cloudbeaver-ce

## Quick Start

```
git clone https://github.com/v1tam1nb2/openresty-cloudbeaver-ce.git
cd openresty-cloudbeaver-ce
docker compose up -d
```

以下にアクセスします。

- [http://localhost:8888/](http://localhost:8888/)
    - 基本認証情報 
        - User:`cbuser`
        - Password:`password`

`New Connection`を選択する。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2518403/45fea9b4-b413-6e28-cb17-bdd573a63535.png)

ClickHouseの例を示します。

`ClickHouse`を選択する。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2518403/984d31cf-db03-9022-b6f8-196da1540e86.png)

以下の情報を入力する。

- Host
    - `clickhouse`
- Port
    - `8123`
- User name
    - `testuser`
- User password
    - `password`

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2518403/ed2a6b12-37e0-ec61-1a6c-78fcbc431825.png)

`TEST`を選択して、接続情報が問題ないか確認する。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2518403/436a90ba-fe25-7a48-0d77-c5d10e541ac2.png)

`Apply`を選択する。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2518403/f56619a5-df53-35ec-4c3d-b1ceca5f7584.png)

以下の表示が出れば問題ない。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2518403/345eb9d9-cb4f-0b55-3593-4eab8d37145b.png)

その後、`CREATE`を選択する。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2518403/f4845907-1078-835e-9591-13f6d8f400f1.png)

`CREATE`後、DBの中身を確認するときに再度認証を求められるので、DBのユーザーとパスワードを入力する。以下のようになればOK。

右上に`cbuser`と表示されていれば、基本認証でログインしたユーザーでCloudbeaverが利用できるようになっています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2518403/6b5c30ec-0801-a5da-dd00-8a5dc289fefa.png)


## compose.yaml

- プロキシの役割として[Openresty](https://openresty.org/en/)を用意（Openresty側で基本認証を行う）
- 接続するDBの例として[ClickHouse](https://clickhouse.com/)を用意

```yaml:compose.yaml
services:
  openresty:
    container_name: openresty
    hostname: openresty
    image: bitnami/openresty:latest
    restart: always
    ports:
      - 8888:80
    volumes:
     - ./src/openresty/nginx.conf:/opt/bitnami/openresty/nginx/conf/nginx.conf
     - ./src/openresty/.htpasswd:/etc/nginx/.htpasswd

  cloudbeaver:
    container_name: cloudbeaver
    hostname: cloudbeaver
    image: dbeaver/cloudbeaver:24.1.3
    restart: always
    volumes:
      - ./src/cloudbeaver/cloudbeaver.conf:/opt/cloudbeaver/conf/cloudbeaver.conf
      - ./cb_data/cloudbeaver/workspace:/opt/cloudbeaver/workspace

  clickhouse:
    image: clickhouse/clickhouse-server:23.3.8.21
    container_name: clickhouse
    hostname: clickhouse
    restart: always
    volumes:
    - ./ch_data/var/lib/clickhouse:/var/lib/clickhouse
    - ./ch_data/var/log/clickhouse-server:/var/log/clickhouse-server
    - ./src/clickhouse/data/etc/clickhouse-server/users.d:/etc/clickhouse-server/users.d
    - ./src/clickhouse/data/etc/clickhouse-server/config.d:/etc/clickhouse-server/config.d
```

## Cloudbeaverの設定

Cloudbeaverの構成情報のドキュメントは以下になります。

https://github.com/dbeaver/cloudbeaver/wiki/Server-configuration

```conf:cloudbeaver.conf
{
    server: {
        serverPort: 8978,
        serverName: "CloudBeaver Sample Server",
        workspaceLocation: "workspace",
        contentRoot: "web",
        driversLocation: "drivers",

        sslConfigurationPath:"${CLOUDBEAVER_SSL_CONF_PATH:workspace/.data/ssl-config.xml}",

        rootURI: "/",
        serviceURI: "/api/",

        productSettings: {
            # Global properties
            core.theming.theme: 'light',
            core.localization.localization: 'en',
            plugin.sql-editor.autoSave: true,
            plugin.sql-editor.disabled: false,
            # max size of the file that can be uploaded to the editor (in kilobytes)
            plugin.sql-editor.maxFileSize: 10240,
            plugin.log-viewer.disabled: false,
            plugin.log-viewer.logBatchSize: 1000,
            plugin.log-viewer.maxFailedRequests: 3,
            plugin.log-viewer.maxLogRecords: 2000,
            plugin.log-viewer.refreshTimeout: 3000,
            sql.proposals.insert.table.alias: PLAIN
        },

        expireSessionAfterPeriod: 1800000,

        develMode: false,

        enableSecurityManager: false,

        sm: {
            enableBruteForceProtection: "${CLOUDBEAVER_BRUTE_FORCE_PROTECTION_ENABLED:true}",
            maxFailedLogin: "${CLOUDBEAVER_MAX_FAILED_LOGINS:10}",
            minimumLoginTimeout: "${CLOUDBEAVER_MINIMUM_LOGIN_TIMEOUT:1}",
            blockLoginPeriod: "${CLOUDBEAVER_BLOCK_PERIOD:300}",
            passwordPolicy: {
                minLength: "${CLOUDBEAVER_POLICY_MIN_LENGTH:8}",
                requireMixedCase: "${CLOUDBEAVER_POLICY_REQUIRE_MIXED_CASE:true}",
                minNumberCount: "${CLOUDBEAVER_POLICY_MIN_NUMBER_COUNT:1}",
                minSymbolCount: "${CLOUDBEAVER_POLICY_MIN_SYMBOL_COUNT:0}"
            }
        },

        database: {
            driver: "${CLOUDBEAVER_DB_DRIVER:h2_embedded_v2}",
            url: "${CLOUDBEAVER_DB_URL:jdbc:h2:${workspace}/.data/cb.h2v2.dat}",
            schema: "${CLOUDBEAVER_DB_SCHEMA:''}",
            user: "${CLOUDBEAVER_DB_USER:''}",
            password: "${CLOUDBEAVER_DB_PASSWORD:''}",
            initialDataConfiguration: "${CLOUDBEAVER_DB_INITIAL_DATA:conf/initial-data.conf}",
            pool: {
                minIdleConnections: "${CLOUDBEAVER_DB_MIN_IDLE_CONNECTIONS:4}",
                maxIdleConnections: "${CLOUDBEAVER_DB_MAX_IDLE_CONNECTIONS:10}",
                maxConnections: "${CLOUDBEAVER_DB_MAX_CONNECTIONS:100}",
                validationQuery: "${CLOUDBEAVER_DB_VALIDATION_QUERY:SELECT 1}"
            },
            backupEnabled: "${CLOUDBEAVER_DB_BACKUP_ENABLED:true}"
        }

    },
    app: {
        anonymousAccessEnabled: true,
        anonymousUserRole: "user",
        defaultUserTeam: "user",
        grantConnectionsAccessToAnonymousTeam: false,
        supportsCustomConnections: true,
        showReadOnlyConnectionInfo: false,
        systemVariablesResolvingEnabled: "${CLOUDBEAVER_SYSTEM_VARIABLES_RESOLVING_ENABLED:false}",

        forwardProxy: true,

        publicCredentialsSaveEnabled: true,
        adminCredentialsSaveEnabled: true,

        resourceManagerEnabled: true,

        resourceQuotas: {
            dataExportFileSizeLimit: 10000000,
            resourceManagerFileSizeLimit: 500000,
            sqlMaxRunningQueries: 100,
            sqlResultSetRowsLimit: 100000,
            sqlResultSetMemoryLimit: 2000000,
            sqlTextPreviewMaxLength: 4096,
            sqlBinaryPreviewMaxLength: 261120
        },
        defaultAuthProvider: "reverseProxy",
        enableReverseProxyAuth: true,
        enabledAuthProviders: [
            "reverseProxy"
        ],

        disabledDrivers: [
            "sqlite:sqlite_jdbc",
            "h2:h2_embedded",
            "h2:h2_embedded_v2",
            "clickhouse:yandex_clickhouse"
        ],
        disabledBetaFeatures: [

        ],
        authConfigurations: [
            {
                "id": "reverseProxy",
                "provider": "reverseProxy",
                "displayName": "Reverse Proxy",
                "disabled": false,
                "iconURL": "",
                "description": "",
                "parameters": {
                    "logout-url": "",
                    "user-header": "",
                    "team-header": "",
                    #"team-delimiter": "",
                    "first-name-header": "",
                    "last-name-header": ""
                }
            }
        ]

    }

}
```

以下の箇所でAuthProvidersを指定します。今回は`reverseProxy`です。
実は、LDAPなどもサポートされていて、`ldap`とすることで、ldap認証も可能です。
```conf
        enabledAuthProviders: [
            "reverseProxy"
        ]
```

リバプロ認証を行うために以下の設定も必要です。`provider`は`reverseProxy`を指定します。

>Important: Ensure you include the mandatory fields id, provider, and displayName. The provider name must be set to reverseProxy.

```conf
        authConfigurations: [
            {
                "id": "reverseProxy",
                "provider": "reverseProxy",
                "displayName": "Reverse Proxy",
                "disabled": false,
                "iconURL": "",
                "description": "",
                "parameters": {
                    "logout-url": "",
                    "user-header": "",
                    "team-header": "",
                    #"team-delimiter": "",
                    "first-name-header": "",
                    "last-name-header": ""
                }
            }
        ]
```
`team-delimiter`をコメントアウトしている理由は、以下のIssueをご確認ください。

https://github.com/dbeaver/cloudbeaver/issues/2803#issuecomment-2255761763

## Openrestyの設定

基本認証の方法については、巷に記事が溢れているので詳細は省略します。

https://qiita.com/kotarella1110/items/be76b17cdbe61ff7b5ca

Openresty to Cloudbeaverの部分について解説します。
大事なのは以下の`location`部分です。

```conf:nginx.confの一部
		location / {
			proxy_pass       http://cloudbeaver:8978;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header Host $http_host;
            proxy_http_version 1.1;
            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header Connection "upgrade";
			proxy_set_header X-User $remote_user;
            proxy_set_header X-Team 'user|admin';

            auth_basic "Restricted";
            auth_basic_user_file /etc/nginx/.htpasswd;

		}
```

- `proxy_pass       http://cloudbeaver:8978;`
   - Docker composeで構築しているので、サービス名で名前解決ができます。
   - Cloudbeaverのデフォルトポートは8978です。

- `proxy_set_header X-User $remote_user;`
    - `$remote_user`で基本認証でログインしたユーザー名を取得します。

- `proxy_set_header X-Team 'user|admin';`
    - ユーザーのチーム（ロール？）を指定します。
    - `user`は一般ユーザー、`admin`は管理者ユーザーの権限が与えられます。
    - 実は、前述した`team-delimiter`でチームの区切り文字を指定できます。デフォルトは`|`です。
    - 管理者権限を与えたくない場合は、`proxy_set_header X-Team user`とすればOKです。

HTTPヘッダーの詳細については以下のドキュメントを参照してください。

https://github.com/dbeaver/cloudbeaver/wiki/Reverse-proxy-header-authentication#configuring-default-http-header-fields

# まとめ

CloudbeaverでReverse proxy header authenticationを実現することができました。

今回は基本認証で試しましたが、Keycloakなどと連携してSSOを実現することも可能です。