What's?
node-postgresを使った時に、SSL接続の構成はどうしたらよいのか調べておきたいなと。
最終的に、ドキュメントをよくよく見るとわかったのだろうという気はするのですが…。
最初はよくわからなかったので、メモとして。
関連するnode-postgresのドキュメント
node-postgresの接続まわりのドキュメントはこちら。
環境変数、API、Connection URIで指定する方法があります。
Connection URIはどのような項目が設定できるのかあまり書かれていないのですが、GitHubのREADME.md
を見るしかなさそうですね。
node-postgresのSSLまわりのドキュメントはこちら。
APIで指定する場合は、ssl
プロパティにtrue
またはfalse
か、CommonConnectionOptions
を渡します。
CommonConnectionOptions
(要はオブジェクト)を指定した場合は、Node.jsのtls.TLSSocket
のコンストラクタに渡されるようです。
new tls.TLSSocket(socket[, options])
Connection URIの場合は、ssl
またはsslmode
で指定します。
この説明は、こちらに書かれています。
pg-connection-string / Connection Strings / TCP Connections
sslmode
の説明は、こちらも参考にするとよいでしょう。
両者の定義は完全には一致しておらず、node-postgresの方はno-verify
というものが増えていますが、これはpg-connection-stringの方に補足が書かれています。
今回は、PostgreSQLをSSL/TLSを有効にした状態で構築し、証明書の検証はパスする(通信の暗号化のみ行う)状態でnode-postgresから接続してみたいと思います。
環境
Node.jsの環境は、こちら。
$ node --version
v18.16.0
$ npm --version
9.5.1
PostgreSQLは、前に書いた以下の記事に沿ってSSL/TLS化含めて構築。
SSL証明書は、この記事で書いた時と同じように自己署名証明書とします。
Node.jsを動かす環境とは別サーバーで動かすものとして、IPアドレスは192.168.30.10とします。
設定は最終的にこうなりました。
$ sudo grep -vE '^\s*#.*$|^$' /var/lib/pgsql/15/data/postgresql.conf
listen_addresses = '*' # what IP address(es) to listen on;
max_connections = 100 # (change requires restart)
ssl = on
ssl_cert_file = '/var/lib/pgsql/15/data/server.crt'
ssl_key_file = '/var/lib/pgsql/15/data/server.key'
shared_buffers = 128MB # min 128kB
dynamic_shared_memory_type = posix # the default is usually the first option
max_wal_size = 1GB
min_wal_size = 80MB
log_destination = 'stderr' # Valid values are combinations of
logging_collector = on # Enable capturing of stderr, jsonlog,
log_directory = 'log' # directory where log files are written,
log_filename = 'postgresql-%a.log' # log file name pattern,
log_rotation_age = 1d # Automatic rotation of logfiles will
log_rotation_size = 0 # Automatic rotation of logfiles will
log_truncate_on_rotation = on # If on, an existing log file with the
log_line_prefix = '%m [%p] ' # special values:
log_timezone = 'Asia/Tokyo'
datestyle = 'iso, mdy'
timezone = 'Asia/Tokyo'
lc_messages = 'C' # locale for system error message
lc_monetary = 'C' # locale for monetary formatting
lc_numeric = 'C' # locale for number formatting
lc_time = 'C' # locale for time formatting
default_text_search_config = 'pg_catalog.english'
firewalldは止めておきます。
$ sudo systemctl stop firewalld
PostgreSQLのバージョン。
$ psql
psql (15.2)
"help"でヘルプを表示します。
postgres=# select version();
version
----------------------------------------------------------------------------------------------------------
PostgreSQL 15.2 on x86_64-pc-linux-gnu, compiled by gcc (GCC) 11.3.1 20220421 (Red Hat 11.3.1-2), 64-bit
(1 行)
利用するアカウントとデータベースの作成。
postgres=# create user myuser password 'password';
CREATE ROLE
postgres=# create database example owner myuser;
CREATE DATABASE
このアカウントおよびデータベースへのTCP接続の場合は、SSL/TLSの利用を必須とします(hostssl
)。
$ sudo grep -vE '^\s*#.*$|^$' /var/lib/pgsql/15/data/pg_hba.conf
local all all peer
hostssl example myuser 192.168.0.0/16 scram-sha-256
host all all 127.0.0.1/32 scram-sha-256
host all all ::1/128 scram-sha-256
local replication all peer
host replication all 127.0.0.1/32 scram-sha-256
host replication all ::1/128 scram-sha-256
psql
でSSL/TLS接続できていることの確認。
$ psql -U myuser -h localhost -p 5432 example
ユーザー myuser のパスワード:
psql (15.2)
SSL接続(プロトコル: TLSv1.3、暗号化方式: TLS_AES_256_GCM_SHA384、圧縮: オフ)
"help"でヘルプを表示します。
example=>
node-postgresを使ってPostgreSQLにSSL/TLS接続する
動作確認するための準備をしていきます。
Node.jsプロジェクトを作成。言語はTypeScriptにして、Jestで動作確認することにします。
$ npm init -y
$ npm i -D typescript
$ npm i -D @types/node@v18
$ npm i -D prettier
$ npm i -D jest @types/jest
$ npm i -D esbuild esbuild-jest
$ mkdir test
node-postgresと型宣言のインストール。
$ npm i pg
$ npm i -D @types/pg
依存関係。
"devDependencies": {
"@types/jest": "^29.5.1",
"@types/node": "^18.16.4",
"@types/pg": "^8.6.6",
"esbuild": "^0.17.18",
"esbuild-jest": "^0.5.0",
"jest": "^29.5.0",
"prettier": "^2.8.8",
"typescript": "^5.0.4"
},
"dependencies": {
"pg": "^8.10.0"
}
scripts
は、こんな感じにしておきました。
"scripts": {
"build": "tsc --project .",
"build:watch": "tsc --project . --watch",
"typecheck": "tsc --project ./tsconfig.typecheck.json",
"typecheck:watch": "tsc --project ./tsconfig.typecheck.json --watch",
"test": "jest",
"format": "prettier --write test"
},
各種設定ファイル。
{
"compilerOptions": {
"target": "esnext",
"module": "commonjs",
"moduleResolution": "node",
"lib": ["esnext"],
"baseUrl": "./",
"outDir": "dist",
"strict": true,
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true,
"noImplicitOverride": true,
"noImplicitReturns": true,
"noPropertyAccessFromIndexSignature": true,
"esModuleInterop": true
},
"include": [
"test"
]
}
{
"extends": "./tsconfig",
"compilerOptions": {
"noEmit": true
}
}
module.exports = {
testEnvironment: 'node',
transform: {
"^.+\\.tsx?$": "esbuild-jest"
}
};
{
"singleQuote": true,
"printWidth": 120
}
それでは、進めていきます。
Connection URIを使う
最初はConnection URIから試してみたいと思います。
以下の3パターンを試しました。
- SSLを有効にしないで接続 → 失敗
- 証明書の検証を行わないモード(
no-verify
)でSSL接続 → 成功 - 証明書の確認を行うモード(
require
)でSSL接続 → 失敗- これは、Node.js側にPostgreSQLにインストールした自己署名証明書を入れていないから
作成したテストコード。
import { Client } from 'pg';
test('connect no ssl', async () => {
const connectionString = 'postgresql://myuser:password@192.168.33.10:5432/example?application_name=app';
const client = new Client({
connectionString,
});
try {
await client.connect();
} catch (e) {
const error = e as Error;
expect(error.message).toBe(
'no pg_hba.conf entry for host "192.168.33.1", user "myuser", database "example", no encryption'
);
}
});
test('connect no-verify ssl', async () => {
const connectionString =
'postgresql://myuser:password@192.168.33.10:5432/example?application_name=app&sslmode=no-verify';
const client = new Client({
connectionString,
});
await client.connect();
const res = await client.query(
`
select
psa.datname, psa.usename, psa.application_name, query, pss.ssl
from
pg_stat_activity psa
inner join
pg_stat_ssl pss
on
psa.pid = pss.pid
where
psa.usename = $1
and psa.application_name = $2
`,
['myuser', 'app']
);
expect(res.rowCount).toBe(1);
const row = res.rows[0];
expect(row.datname).toBe('example');
expect(row.usename).toBe('myuser');
expect(row.ssl).toBeTruthy();
await client.end();
});
test('connect require ssl', async () => {
const connectionString =
'postgresql://myuser:password@192.168.33.10:5432/example?application_name=app&sslmode=require';
const client = new Client({
connectionString,
});
try {
await client.connect();
} catch (e) {
const error = e as Error;
expect(error.message).toBe('self-signed certificate');
}
});
Connection URIに指定している、sslmode
がポイントですね。
2つ目のパターンでは接続に成功するのでpg_stat_activity
とpg_stat_ssl
から、SSLでの接続状態を確認しています。
3つ目は、PostgreSQLサーバー側が自己署名証明書なのでエラーになっています…。
これをちゃんとするのは、今回はやりません。
APIで指定する
同じことをAPI指定でやってみます。
import { Client } from 'pg';
test('connect no ssl', async () => {
const client = new Client({
user: 'myuser',
password: 'password',
database: 'example',
host: '192.168.33.10',
port: 5432,
application_name: 'app',
});
try {
await client.connect();
} catch (e) {
const error = e as Error;
expect(error.message).toBe(
'no pg_hba.conf entry for host "192.168.33.1", user "myuser", database "example", no encryption'
);
}
});
test('connect no-verify ssl', async () => {
const client = new Client({
user: 'myuser',
password: 'password',
database: 'example',
host: '192.168.33.10',
port: 5432,
application_name: 'app',
ssl: {
rejectUnauthorized: false
},
});
await client.connect();
const res = await client.query(
`
select
psa.datname, psa.usename, psa.application_name, query, pss.ssl
from
pg_stat_activity psa
inner join
pg_stat_ssl pss
on
psa.pid = pss.pid
where
psa.usename = $1
and psa.application_name = $2
`,
['myuser', 'app']
);
expect(res.rowCount).toBe(1);
const row = res.rows[0];
expect(row.datname).toBe('example');
expect(row.usename).toBe('myuser');
expect(row.ssl).toBeTruthy();
await client.end();
});
test('connect require ssl', async () => {
const client = new Client({
user: 'myuser',
password: 'password',
database: 'example',
host: '192.168.33.10',
port: 5432,
application_name: 'app',
ssl: {
rejectUnauthorized: true,
},
});
try {
await client.connect();
} catch (e) {
const error = e as Error;
expect(error.message).toBe('self-signed certificate');
}
});
sslmode=no-verify
相当のものはどう指定したらいいんだろう?というのにちょっと迷いましたが、これはssl
でrejectUnauthorized
で調整すると良さそうです。
よくよく見ると、こちらにも同じことが書かれています。
pg-connection-string / Connection Strings / TCP Connections
rejectUnauthorized
の意味は、SSL接続に使用するCAリストで許可されていない接続を拒否する、です。