PostgreSQL
 sql >> Base de Dados >  >> RDS >> PostgreSQL

Autoprovisionamento de contas de usuário no PostgreSQL via acesso anônimo não privilegiado


Nota de Variousnines:Este blog está sendo publicado postumamente quando Berend Tober faleceu em 16 de julho de 2018. Honramos suas contribuições para a comunidade PostgreSQL e desejamos paz para nosso amigo e escritor convidado.

No artigo anterior, apresentamos os fundamentos dos gatilhos e funções armazenadas do PostgreSQL e fornecemos seis casos de uso de exemplo, incluindo validação de dados, registro de alterações, derivação de valores de dados inseridos, ocultação de dados com visualizações atualizáveis ​​simples, manutenção de dados de resumo em tabelas separadas e invocação segura de código com privilégio elevado. Este artigo se baseia ainda mais nessa base e apresenta uma técnica que utiliza um gatilho e uma função armazenada para facilitar a delegação do provisionamento de credenciais de login para funções de privilégio limitado (ou seja, não superusuário). Esse recurso pode ser usado para reduzir a carga de trabalho administrativa para a equipe de administração de sistemas de alto valor. Levado ao extremo, demonstramos o autoprovisionamento anônimo de credenciais de login pelo usuário final, ou seja, permitindo que usuários de banco de dados em potencial forneçam credenciais de login por conta própria implementando “SQL dinâmico” dentro de uma função armazenada executada no nível de privilégio com escopo apropriado.Introdução

Leitura de fundo útil


O artigo recente de Sebastian Insausti sobre Como proteger seu banco de dados PostgreSQL inclui algumas dicas altamente relevantes com as quais você deve se familiarizar, a saber, Dicas #1 - #5 sobre controle de autenticação de cliente, configuração do servidor, gerenciamento de usuários e funções, gerenciamento de superusuários e Criptografia de dados. Usaremos partes de cada dica neste artigo.

Outro artigo recente de Joshua Otwell sobre Privilégios e gerenciamento de usuários do PostgreSQL também apresenta um bom tratamento da configuração do host e dos privilégios do usuário que detalha um pouco mais esses dois tópicos.

Protegendo o tráfego de rede


O recurso proposto envolve permitir que os usuários forneçam credenciais de login do banco de dados e, ao fazê-lo, eles especificarão seu novo nome de login e senha pela rede. A proteção dessa comunicação de rede é essencial e pode ser obtida configurando o servidor PostgreSQL para suportar e exigir conexões criptografadas. A segurança da camada de transporte é habilitada no arquivo postgresql.conf pela configuração “ssl”:
ssl = on

Controle de acesso baseado em host


Para o presente caso, adicionaremos uma linha de configuração de acesso baseado em host no arquivo pg_hba.conf que permite login anônimo, ou seja, confiável, no banco de dados de alguma sub-rede apropriada para a população de usuários potenciais do banco de dados literalmente usando o nome de usuário “anonymous” e uma segunda linha de configuração exigindo login de senha para qualquer outro nome de login. Lembre-se de que as configurações do host invocam a primeira correspondência, portanto, a primeira linha será aplicada sempre que o nome de usuário “anônimo” for especificado, permitindo uma conexão confiável (ou seja, sem necessidade de senha) e, posteriormente, sempre que qualquer outro nome de usuário for especificado, uma senha será necessária. Por exemplo, se o banco de dados de amostra “sampledb” for usado, digamos, apenas por funcionários e internamente às instalações corporativas, podemos configurar o acesso confiável para alguma sub-rede interna não roteável com:
# TYPE  DATABASE USER      ADDRESS        METHOD
hostssl sampledb anonymous 192.168.1.0/24 trust
hostssl sampledb all       192.168.1.0/24 md5

Se o banco de dados for disponibilizado ao público em geral, podemos configurar o acesso a “qualquer endereço”:
# TYPE  DATABASE USER       ADDRESS  METHOD
hostssl sampledb anonymous  all      trust
hostssl sampledb all        all      md5

Observe que o acima é potencialmente perigoso sem precauções adicionais, possivelmente no design do aplicativo ou em um dispositivo de firewall, para limitar o uso desse recurso, porque você sabe que algum script kiddie automatizará a criação infinita de contas apenas para o lulz.

Observe também que especificamos o tipo de conexão como “hostssl”, o que significa que as conexões feitas usando TCP/IP são bem-sucedidas apenas quando a conexão é feita com criptografia SSL para proteger o tráfego da rede contra espionagem.

Bloqueando o esquema público


Como estamos permitindo que pessoas possivelmente desconhecidas (ou seja, não confiáveis) acessem o banco de dados, queremos ter certeza de que os acessos padrão são limitados em capacidade. Uma medida importante é revogar o privilégio de criação de objeto de esquema público padrão para mitigar uma vulnerabilidade do PostgreSQL recentemente publicada relacionada a privilégios de esquema padrão (cf. Bloqueando o esquema público por si mesmo).

Um banco de dados de amostra


Começaremos com um banco de dados de amostra vazio para fins de ilustração:
create database sampledb;
\connect sampledb

revoke create on schema public from public;
alter default privileges revoke all privileges on tables from public;

Também criamos a função de login anônimo correspondente à configuração anterior do pg_hba.conf.
create role anonymous login
    nosuperuser 
    noinherit 
    nocreatedb 
    nocreaterole 
    Noreplication;

E então fazemos algo novo definindo uma visão não convencional:
create or replace view person as 
 select 
    null::name as login_name,
    null::name as login_pass;

Esta visualização não faz referência a nenhuma tabela e, portanto, uma consulta de seleção sempre retorna uma linha vazia:
select * from person;
 login_name | login_pass 
------------+-------------
            | 
(1 row)

Uma coisa que isso faz para nós é fornecer documentação ou uma dica para os usuários finais sobre quais dados são necessários para estabelecer uma conta. Ou seja, ao consultar a tabela, mesmo que o resultado seja uma linha vazia, o resultado revela os nomes dos dois elementos de dados.

Mas ainda melhor, a existência desta visão permite a determinação dos tipos de dados necessários:
\d person
      View "public.person"
    Column    | Type | Modifiers 
--------------+------+-----------
 login_name   | name | 
 login_pass   | name | 

Estaremos implementando a funcionalidade de provisionamento de credenciais com uma função e gatilho armazenados, então vamos declarar um modelo de função vazio e o gatilho associado:
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();

Observe que estamos seguindo a convenção de nomenclatura proposta do artigo anterior, usando o nome da tabela associada com o sufixo de uma abreviação que denota atributos do relacionamento de gatilho entre a tabela e a função armazenada para um gatilho INSTEAD OF INSERT (ou seja, sufixo “ ei”). Também adicionamos à função armazenada os atributos SCHEMA e SECURITY DEFINER:o primeiro porque é uma boa prática definir o caminho de pesquisa que se aplica à duração da execução da função e o último para facilitar a criação de função, que normalmente é uma autoridade de superusuário do banco de dados apenas, mas neste caso será delegado a usuários anônimos.

E, por último, adicionamos permissões minimamente suficientes na visualização para consultar e inserir:
grant select, insert on table person to anonymous;
Baixe o whitepaper hoje PostgreSQL Management &Automation with ClusterControlSaiba o que você precisa saber para implantar, monitorar, gerenciar e dimensionar o PostgreSQLBaixe o whitepaper

Vamos analisar


Antes de implementar o código da função armazenada, vamos revisar o que temos. Primeiro, há o banco de dados de exemplo de propriedade do usuário 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 | {}

E há a visão que criamos e uma lista de privilégios de acesso de criação e leitura concedidos ao usuário anônimo pelo usuário 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)

Por fim, o detalhe da tabela mostra os nomes das colunas e os tipos de dados, bem como o gatilho associado:
\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 Dinâmico


Vamos empregar SQL dinâmico, ou seja, construir a forma final de uma instrução DDL em tempo de execução parcialmente a partir de dados inseridos pelo usuário, para preencher o corpo da função de gatilho. Especificamente, codificamos o esboço da instrução para criar uma nova função de login e preencher os parâmetros específicos como variáveis.

A forma geral deste comando é
create role name [ [ with ] option [ ... ] ]

onde opção pode ser qualquer uma das dezesseis propriedades específicas. Geralmente os padrões são apropriados, mas vamos ser explícitos sobre várias opções de limitação e usar o formulário
create role name 
  with 
    login 
    inherit 
    nosuperuser 
    nocreatedb 
    nocreaterole 
    password ‘password’;

onde inseriremos o nome da função e a senha especificados pelo usuário em tempo de execução.

As instruções construídas dinamicamente são invocadas com o comando execute:
execute command-string [ INTO [STRICT] target ] [ USING expression [, ... ] ];

que para nossas necessidades específicas seria
  execute 'create role '
    || new.login_name
    || ' with login inherit nosuperuser nocreatedb nocreaterole password '
    || quote_literal(new.login_pass);

onde a função quote_literal retorna o argumento de string adequadamente citado para uso como um literal de string para cumprir o requisito sintático de que a senha seja de fato citada.

Uma vez que tenhamos a string de comando construída, nós a fornecemos como o argumento para o comando pl/pgsql execute dentro da função trigger.

Juntando tudo isso fica:
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;
  $$;

Vamos tentar!


Tudo está no lugar, então vamos dar um giro! Primeiro, alternamos a autorização de sessão para o usuário anônimo e, em seguida, fazemos uma inserção na visualização da pessoa:
set session authorization anonymous;
insert into person values ('alice', '1234');

O resultado é que o novo usuário alice foi adicionado à tabela do sistema:
\du
                                   List of roles
 Role name |                         Attributes                         | Member of 
-----------+------------------------------------------------------------+-----------
 alice     |                                                            | {}
 anonymous | No inheritance                                             | {}
 postgres  | Superuser, Create role, Create DB, Replication, Bypass RLS | {}

Ele ainda funciona diretamente a partir da linha de comando do sistema operacional, canalizando uma string de comando SQL para o utilitário de cliente psql para adicionar o usuário 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 | {}

Aplique um pouco de armadura


O exemplo inicial da função de gatilho é vulnerável ao ataque de injeção de SQL, ou seja, um agente de ameaça mal-intencionado pode criar uma entrada que resulta em acesso não autorizado. Por exemplo, enquanto conectado como a função de usuário anônimo, uma tentativa de fazer algo fora do escopo falha adequadamente:
set session authorization anonymous;
drop user alice;
ERROR:  permission denied to drop role

Mas a seguinte entrada maliciosa cria uma função de superusuário chamada 'eve' (assim como uma conta chamariz chamada '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 | {}

Em seguida, a função de superusuário sub-reptício pode ser usada para causar estragos no banco de dados, por exemplo, excluindo contas de usuário (ou pior!):
\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 | {}

Para mitigar essa vulnerabilidade, devemos tomar medidas para limpar a entrada. Por exemplo, aplicando a função quote_ident, que retorna uma string adequadamente citada para uso como identificador em uma instrução SQL com aspas adicionadas quando necessário, como se a string contivesse caracteres não identificadores ou fosse dobrada em maiúsculas, e duplicando corretamente incorporado citações:
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;
  $$;

Agora, se o mesmo exploit de injeção de SQL tentar criar outro superusuário chamado 'frank', ele falhará e o resultado será um nome de usuário muito pouco ortodoxo:
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 | {}

Podemos aplicar mais validação de dados sensíveis dentro da função de gatilho, como exigir apenas nomes de usuário alfanuméricos e rejeitar espaços em branco e outros caracteres:
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;
  $$;

e, em seguida, confirme se as várias verificações de higienização funcionam:
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.

Vamos aumentar ainda mais


Suponha que queremos armazenar metadados adicionais ou dados de aplicativos relacionados à função de usuário criada, por exemplo, talvez um carimbo de data/hora e endereço IP de origem associado à criação da função. É claro que a exibição não pode satisfazer esse novo requisito, pois não há armazenamento subjacente, portanto, uma tabela real é necessária. Além disso, vamos supor que queremos restringir a visibilidade dessa tabela de usuários que efetuam login com a função de login anônimo. Podemos ocultar a tabela em um namespace separado (ou seja, um esquema PostgreSQL) que permanece inacessível para usuários anônimos. Vamos chamar esse namespace de namespace “privado” e criar a tabela no namespace:
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()  
);

Um simples comando de inserção adicional dentro da função de gatilho registra esses metadados associados:
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;
  $$;

E podemos fazer um teste fácil. Primeiro, confirmamos que, enquanto conectado como a função anônima, apenas a visualização public.person é visível e não a tabela 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

E depois de inserir uma nova função:
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)

a tabela private.person mostra a captura de metadados para o endereço IP e o tempo de inserção da linha.

Conclusão


Neste artigo, demonstramos uma técnica para delegar o provisionamento de credenciais de função do PostgreSQL a funções de não superusuário. Embora o exemplo delegue totalmente a funcionalidade de credenciamento a usuários anônimos, uma abordagem semelhante pode ser usada para delegar parcialmente a funcionalidade apenas a pessoal confiável, mantendo o benefício de descarregar esse trabalho do banco de dados de alto valor ou do pessoal do administrador de sistemas. Também demonstramos uma técnica de acesso a dados em camadas utilizando esquemas PostgreSQL, expondo ou ocultando seletivamente objetos de banco de dados. No próximo artigo desta série, expandiremos a técnica de acesso a dados em camadas para propor um novo design de arquitetura de banco de dados para implementações de aplicativos.