Someninesからのメモ:このブログは、Berend Toberが2018年7月16日に亡くなったため、死後に公開されています。PostgreSQLコミュニティへの彼の貢献を称え、友人とゲストライターの平和を願っています。
前回の記事では、PostgreSQLトリガーと保存された関数の基本を紹介し、データ検証、変更ログ、挿入されたデータからの値の導出、単純な更新可能なビューでのデータの非表示、要約データの維持など、6つのユースケースの例を示しました。別のテーブルで、昇格された特権でコードを安全に呼び出します。この記事は、その基盤をさらに発展させ、トリガーとストアド機能を利用して、ログイン資格情報のプロビジョニングを制限付き特権(つまり、非スーパーユーザー)の役割に委任することを容易にする手法を示します。この機能は、価値の高いシステム管理者の管理作業負荷を軽減するために使用される可能性があります。極端に言えば、ログイン資格情報の匿名のエンドユーザーセルフプロビジョニングを示します。つまり、適切なスコープの特権レベルで実行されるストアド関数内に「動的SQL」を実装することで、将来のデータベースユーザーが自分でログイン資格情報をプロビジョニングできるようにします。はじめに
役立つ背景資料
Sebastian InsaustiによるPostgreSQLデータベースの保護方法に関する最近の記事には、よく知っておくべき関連性の高いヒントがいくつか含まれています。つまり、クライアント認証制御、サーバー構成、ユーザーと役割の管理、スーパーユーザー管理、およびデータ暗号化。この記事では、各ヒントの一部を使用します。
Joshua OtwellによるPostgreSQLの特権とユーザー管理に関する別の最近の記事でも、ホスト構成とユーザー特権について適切に扱っており、これら2つのトピックについてもう少し詳しく説明しています。
ネットワークトラフィックの保護
提案された機能には、ユーザーがデータベースのログイン資格情報をプロビジョニングできるようにすることが含まれ、その間、ユーザーはネットワークを介して新しいログイン名とパスワードを指定します。このネットワーク通信の保護は不可欠であり、暗号化された接続をサポートおよび要求するようにPostgreSQLサーバーを構成することで実現できます。トランスポート層のセキュリティは、postgresql.confファイルで「ssl」設定によって有効になります。
ssl = on
ホストベースのアクセス制御
この場合、pg_hba.confファイルにホストベースのアクセス構成行を追加します。これにより、匿名、つまり信頼できる、適切なサブネットワークからデータベースへのログインが可能になり、将来のデータベースユーザーの集団が文字通りユーザー名を使用できるようになります。 「匿名」、および他のログイン名のパスワードログインを必要とする2番目の構成行。ホスト構成は最初の一致を呼び出すため、「匿名」ユーザー名が指定された場合は常に最初の行が適用され、信頼できる(つまり、パスワードは不要)接続が許可され、その後、他のユーザー名が指定された場合は常にパスワードが必要になります。たとえば、サンプルデータベース「sampledb」を従業員のみが使用し、企業施設の内部で使用する場合は、次のようにルーティングできない内部サブネットの信頼できるアクセスを構成できます。
# TYPE DATABASE USER ADDRESS METHOD
hostssl sampledb anonymous 192.168.1.0/24 trust
hostssl sampledb all 192.168.1.0/24 md5
データベースを一般に公開する場合は、「任意のアドレス」アクセスを構成できます。
# TYPE DATABASE USER ADDRESS METHOD
hostssl sampledb anonymous all trust
hostssl sampledb all all md5
上記は、アプリケーションの設計やファイアウォールデバイスで、この機能の使用をレート制限するための追加の予防措置なしに潜在的に危険であることに注意してください。スクリプトキディがlulzのためだけに無限のアカウント作成を自動化することを知っているからです。
また、接続タイプを「hostssl」として指定しました。これは、ネットワークトラフィックを盗聴から保護するために、SSL暗号化を使用して接続した場合にのみTCP/IPを使用して接続が成功することを意味します。
パブリックスキーマのロックダウン
未知の(つまり、信頼できない)人がデータベースにアクセスすることを許可しているため、デフォルトのアクセスは機能が制限されていることを確認する必要があります。重要な対策の1つは、デフォルトのパブリックスキーマオブジェクト作成特権を取り消して、デフォルトのスキーマ特権に関連する最近公開されたPostgreSQLの脆弱性を軽減することです(実際にパブリックスキーマをロックダウンするを参照)。
サンプルデータベース
説明のために、空のサンプルデータベースから始めます。
create database sampledb;
\connect sampledb
revoke create on schema public from public;
alter default privileges revoke all privileges on tables from public;
また、以前のpg_hba.conf設定に対応する匿名ログインロールを作成します。
create role anonymous login
nosuperuser
noinherit
nocreatedb
nocreaterole
Noreplication;
そして、型破りな見方を定義することで、何か新しいことをします。
create or replace view person as
select
null::name as login_name,
null::name as login_pass;
このビューはテーブルを参照しないため、selectクエリは常に空の行を返します:
select * from person;
login_name | login_pass
------------+-------------
|
(1 row)
これが私たちのために行うことの1つは、アカウントを確立するために必要なデータに関するドキュメントまたはヒントをエンドユーザーに提供するという意味です。つまり、テーブルをクエリすると、結果が空の行であっても、結果には2つのデータ要素の名前が表示されます。
しかし、さらに良いことに、このビューの存在により、必要なデータ型を決定できます。
\d person
View "public.person"
Column | Type | Modifiers
--------------+------+-----------
login_name | name |
login_pass | name |
ストアド関数とトリガーを使用してクレデンシャルプロビジョニング機能を実装するので、空の関数テンプレートと関連するトリガーを宣言しましょう:
create or replace function person_iit()
returns trigger
set schema 'public'
language plpgsql
security definer
as '
begin
end;
';
create trigger person_iit
instead of insert
on person
for each row execute procedure person_iit();
前回の記事で提案された命名規則に従っており、テーブルとINSTEAD OF INSERTトリガーのストアド関数のトリガー関係の属性を示す短い省略形が接尾辞として付いた関連テーブル名を使用していることに注意してください(つまり、接尾辞「 iit」)。また、ストアド関数にSCHEMA属性とSECURITY DEFINER属性を追加しました。前者は関数の実行期間に適用される検索パスを設定するのが良い方法であり、後者は役割の作成を容易にするためです。これは通常、データベースのスーパーユーザー権限です。ただし、この場合は匿名ユーザーに委任されます。
最後に、クエリと挿入を行うために、ビューに最小限の権限を追加します。
grant select, insert on table person to anonymous;
今日のホワイトペーパーをダウンロードするClusterControlを使用したPostgreSQLの管理と自動化PostgreSQLの導入、監視、管理、スケーリングを行うために知っておくべきことについて学ぶホワイトペーパーをダウンロードする 確認しましょう
ストアドファンクションコードを実装する前に、私たちが持っているものを確認しましょう。まず、postgresユーザーが所有するサンプルデータベースがあります:
\l
List of databases
Name | Owner | Encoding | Collate | Ctype | Access privileges
-----------+----------+----------+-------------+-------------+-----------------------
sampledb | postgres | UTF8 | en_US.UTF-8 | en_US.UTF-8 |
And there’s the user roles, including the database superuser and the newly-created anonymous login roles:
\du
List of roles
Role name | Attributes | Member of
-----------+------------------------------------------------------------+-----------
anonymous | No inheritance | {}
postgres | Superuser, Create role, Create DB, Replication, Bypass RLS | {}
そして、私たちが作成したビューと、postgresユーザーによって匿名ユーザーに付与された作成および読み取りアクセス権のリストがあります:
\d
List of relations
Schema | Name | Type | Owner
--------+--------+------+----------
public | person | view | postgres
(1 row)
\dp
Access privileges
Schema | Name | Type | Access privileges | Column privileges | Policies
--------+--------+------+---------------------------+-------------------+----------
public | person | view | postgres=arwdDxt/postgres+| |
| | | anonymous=ar/postgres | |
(1 row)
最後に、テーブルの詳細には、列名とデータ型、および関連するトリガーが表示されます。
\d person
View "public.person"
Column | Type | Modifiers
--------------+------+-----------
login_name | name |
login_pass | name |
Triggers:
person_iit INSTEAD OF INSERT ON person FOR EACH ROW EXECUTE PROCEDURE person_iit()
動的SQL
動的SQLを使用します。つまり、実行時にユーザーが入力したデータから部分的にDDLステートメントの最終形式を作成して、トリガー関数の本体に入力します。具体的には、ステートメントのアウトラインをハードコーディングして、新しいログインロールを作成し、特定のパラメーターを変数として入力します。
このコマンドの一般的な形式は
です。create role name [ [ with ] option [ ... ] ]
ここでオプション 16の特定のプロパティのいずれかになります。通常、デフォルトが適切ですが、いくつかの制限オプションについて明示し、フォームを使用します
create role name
with
login
inherit
nosuperuser
nocreatedb
nocreaterole
password ‘password’;
ここに、実行時にユーザー指定の役割名とパスワードを挿入します。
動的に構築されたステートメントは、executeコマンドで呼び出されます:
execute command-string [ INTO [STRICT] target ] [ USING expression [, ... ] ];
私たちの特定のニーズには次のようになります
execute 'create role '
|| new.login_name
|| ' with login inherit nosuperuser nocreatedb nocreaterole password '
|| quote_literal(new.login_pass);
ここで、quote_literal関数は、パスワードが実際に引用されるという構文上の要件に準拠するために、文字列リテラルとして使用するために適切に引用された文字列引数を返します。
コマンド文字列を作成したら、それをトリガー関数内のpl/pgsql実行コマンドの引数として指定します。
これをすべてまとめると、次のようになります。
create or replace function person_iit()
returns trigger
set schema 'public'
language plpgsql
security definer
as $$
begin
-- note this is for demonstration only. it is vulnerable to sql injection.
execute 'create role '
|| new.login_name
|| ' with login inherit nosuperuser nocreatedb nocreaterole password '
|| quote_literal(new.login_pass);
return new;
end;
$$;
やってみよう!
すべてが整っているので、回転させましょう。まず、セッション認証を匿名ユーザーに切り替えてから、個人ビューに対して挿入を行います。
set session authorization anonymous;
insert into person values ('alice', '1234');
その結果、新しいユーザーaliceがシステムテーブルに追加されました:
\du
List of roles
Role name | Attributes | Member of
-----------+------------------------------------------------------------+-----------
alice | | {}
anonymous | No inheritance | {}
postgres | Superuser, Create role, Create DB, Replication, Bypass RLS | {}
SQLコマンド文字列をpsqlクライアントユーティリティにパイプしてユーザーbobを追加することにより、オペレーティングシステムのコマンドラインから直接機能することもできます。
$ psql sampledb anonymous <<< "insert into person values ('bob', '4321');"
INSERT 0 1
$ psql sampledb anonymous <<< "\du"
List of roles
Role name | Attributes | Member of
-----------+------------------------------------------------------------+-----------
alice | | {}
anonymous | No inheritance | {}
bob | | {}
postgres | Superuser, Create role, Create DB, Replication, Bypass RLS | {}
鎧を塗る
トリガー関数の最初の例は、SQLインジェクション攻撃に対して脆弱です。つまり、悪意のある脅威アクターが入力を作成して、不正アクセスを引き起こす可能性があります。たとえば、匿名ユーザーロールとして接続しているときに、範囲外のことを実行しようとすると、適切に失敗します。
set session authorization anonymous;
drop user alice;
ERROR: permission denied to drop role
ただし、次の悪意のある入力により、「eve」という名前のスーパーユーザーロール(および「cathy」という名前のおとりアカウント)が作成されます。
insert into person
values ('eve with superuser login password ''666''; create role cathy', '777');
\du
List of roles
Role name | Attributes | Member of
-----------+------------------------------------------------------------+-----------
alice | | {}
anonymous | No inheritance | {}
cathy | | {}
eve | Superuser | {}
postgres | Superuser, Create role, Create DB, Replication, Bypass RLS | {}
次に、不正なスーパーユーザーの役割を使用して、データベースに大混乱をもたらすことができます。たとえば、ユーザーアカウントを削除する(またはさらに悪いことに):
\c - eve
drop user alice;
\du
List of roles
Role name | Attributes | Member of
-----------+------------------------------------------------------------+-----------
anonymous | No inheritance | {}
cathy | | {}
eve | Superuser | {}
postgres | Superuser, Create role, Create DB, Replication, Bypass RLS | {}
この脆弱性を軽減するには、入力をサニタイズするための手順を実行する必要があります。たとえば、quote_ident関数を適用すると、SQLステートメントで識別子として使用するために適切に引用された文字列が返され、文字列に非識別子文字が含まれている場合や大文字と小文字が区別される場合など、必要に応じて引用符が追加され、適切に2倍に埋め込まれます。引用符:
create or replace function person_iit()
returns trigger
set schema 'public'
language plpgsql
security definer
as $$
begin
execute 'create role '
|| quote_ident(new.login_name)
|| ' with login inherit nosuperuser nocreatedb nocreaterole password '
|| quote_literal(new.login_pass);
return new;
end;
$$;
これで、同じSQLインジェクションのエクスプロイトが「frank」という名前の別のスーパーユーザーを作成しようとすると失敗し、結果は非常に非正統的なユーザー名になります。
set session authorization anonymous;
insert into person
values ('frank with superuser login password ''666''; create role dave', '777');
\du
List of roles
Role name | Attributes | Member of
-----------------------+------------------------------------------------------------+----------
anonymous | No inheritance | {}
eve | Superuser | {}
frank with superuser | |
login password '666';| |
create role dave | |
postgres | Superuser, Create role, Create DB, Replication, Bypass RLS | {}
英数字のユーザー名のみを要求し、空白やその他の文字を拒否するなど、トリガー機能内でさらに賢明なデータ検証を適用できます。
create or replace function person_iit()
returns trigger
set schema 'public'
language plpgsql
security definer
as $$
begin
-- Basic input sanitization
if new.login_name is null then
raise exception 'null login_name disallowed';
elsif position(' ' in new.login_name) > 0 then
raise exception 'login_name whitespace disallowed';
elsif length(new.login_name) = 0 then
raise exception 'login_name must be non-empty';
elsif not (select new.login_name similar to '[A-Za-z]%') then
raise exception 'login_name must begin with a letter.';
end if;
if new.login_pass is null then
raise exception 'null login_pass disallowed';
elsif position(' ' in new.login_pass) > 0 then
raise exception 'login_pass whitespace disallowed';
elsif length(new.login_pass) = 0 then
raise exception 'login_pass must be non-empty';
end if;
-- Provision login credentials
execute 'create role '
|| quote_ident(new.login_name)
|| ' with login inherit nosuperuser nocreatedb nocreaterole password '
|| quote_literal(new.login_pass);
return new;
end;
$$;
次に、さまざまな消毒チェックが機能することを確認します。
set session authorization anonymous;
insert into person values (NULL, NULL);
ERROR: null login_name disallowed
insert into person values ('gina', NULL);
ERROR: null login_pass disallowed
insert into person values ('gina', '');
ERROR: login_pass must be non-empty
insert into person values ('', '1234');
ERROR: login_name must be non-empty
insert into person values ('gi na', '1234');
ERROR: login_name whitespace disallowed
insert into person values ('1gina', '1234');
ERROR: login_name must begin with a letter.
ワンランク上のステップにしましょう
作成されたユーザーロールに関連する追加のメタデータまたはアプリケーションデータを保存するとします。たとえば、ロールの作成に関連付けられたタイムスタンプとソースIPアドレスなどです。もちろん、基になるストレージがないため、ビューはこの新しい要件を満たすことができません。したがって、実際のテーブルが必要です。また、匿名ログインロールでログインしているユーザーからのそのテーブルの表示を制限したいとします。匿名ユーザーがアクセスできないままの別の名前空間(つまり、PostgreSQLスキーマ)でテーブルを非表示にすることができます。この名前空間を「プライベート」名前空間と呼び、名前空間にテーブルを作成しましょう。
create schema private;
create table private.person (
login_name name not null primary key,
inet_client_addr inet default inet_client_addr(),
create_time timestamptz default now()
);
トリガー関数内の単純な追加の挿入コマンドは、この関連するメタデータを記録します:
create or replace function person_iit()
returns trigger
set schema 'public'
language plpgsql
security definer
as $$
begin
-- Basic input sanitization
if new.login_name is null then
raise exception 'null login_name disallowed';
elsif position(' ' in new.login_name) > 0 then
raise exception 'login_name whitespace disallowed';
elsif length(new.login_name) = 0 then
raise exception 'login_name must be non-empty';
elsif not (select new.login_name similar to '[A-Za-z]%') then
raise exception 'login_name must begin with a letter.';
end if;
if new.login_pass is null then
raise exception 'null login_pass disallowed';
elsif length(new.login_pass) = 0 then
raise exception 'login_pass must be non-empty';
end if;
-- Record associated metadata
insert into private.person values (new.login_name);
-- Provision login credentials
execute 'create role '
|| quote_ident(new.login_name)
|| ' with login inherit nosuperuser nocreatedb nocreaterole password '
|| quote_literal(new.login_pass);
return new;
end;
$$;
そして、私たちはそれに簡単なテストを与えることができます。まず、匿名の役割として接続されている間は、public.personビューのみが表示され、private.personテーブルは表示されないことを確認します。
set session authorization anonymous;
\d
List of relations
Schema | Name | Type | Owner
--------+--------+------+----------
public | person | view | postgres
(1 row)
select * from private.person;
ERROR: permission denied for schema private
そして、新しい役割の挿入後:
insert into person values ('gina', '1234');
reset session authorization;
select * from private.person;
login_name | inet_client_addr | create_time
------------+------------------+-------------------------------
gina | 192.168.2.106 | 2018-06-24 07:56:13.838679-07
(1 row)
private.personテーブルには、IPアドレスのメタデータキャプチャと行の挿入時間が表示されます。
結論
この記事では、PostgreSQLの役割の資格情報のプロビジョニングをスーパーユーザー以外の役割に委任する手法を示しました。この例では、資格情報機能を匿名ユーザーに完全に委任しましたが、同様のアプローチを使用して、信頼できる担当者のみに機能を部分的に委任する一方で、価値の高いデータベースまたはシステム管理者の担当者からこの作業をオフロードするメリットを維持できます。また、PostgreSQLスキーマを利用して、データベースオブジェクトを選択的に公開または非表示にする、階層化されたデータアクセスの手法についても説明しました。このシリーズの次の記事では、階層化されたデータアクセス手法を拡張して、アプリケーション実装用の新しいデータベースアーキテクチャ設計を提案します。