Доступ к PostgreSQL с помощью JDBC через Java SSL

ArticleCategory: Software Development

SystemAdministration

AuthorImage:[Here we need a little image from you]

[Photo of the Author]

TranslationInfo:[Author + translation history. mailto: or http://homepage]

original in en Chianglin Ng 

en to ru Pukhlyakov Kirill 

AboutTheAuthor:[A small biography about the author]

Я живу с Сингапуре - современном, многонациональном государстве, расположеннном в южной Азии. ОС Linux я использую на протяжении 2 лет. Первым моим дистрибутивом был redhat 6.2. В настоящее время дома я использую redhat 8.0.

Abstract:[Here you write a little summary]

В заметке рассказывается о доступе к PostgreSQL через JDBC в redhat 8.0 используя расширения Sun's Java Secured Socket для обеспечения безопасного доступа к удаленной базе данных.

ArticleIllustration:[One image that will end up at the top of the article]

[Illustration]

ArticleBody:[The main part of the article]

Введение

Изучая работу связки postgres и JDBC у меня возник вопрос обеспечения безопасного доступа к данным.    
Обычно JDBC соединения не зашифрованы и любой злоумышленник может получить доступ к важным данным. Существует несколько способов решения данной проблемы. Документация postgres говорит о том, что можно скомпилировать postgres с поддержкой SSL или использовать SSH tunneling.

Вместо этих двух способов я решил использовать возможности java. В состав Sun's Java JDK 1.4.1 включены java secured socket extensions (JSSE), которые могут быть использованы для обеспечения SSL соединений. Также JDK предоставляет инструмент для создания public/private ключей, цифровых сертификатов и хранилищ. Следовательно, все инструменты для создания java-прокси есть, которые и будут передавать данные.

Установка PostgreSQL с JDBC в redhat 8.0

Данные инструкции для redhat 8.0, но в общем применимы ко всем дистрибутивам. Прежде всего необходимо установить PostgreSQL и соответствующие JDBC драйверы если вы еще не сделали этого. В redhat 8.0, вы можете использовать команду rpm или графический инструмент для управления пакетами. Также установите Sun's JDK 1.4.1. Sun's JDK 1.4.1 распространяется с ограничениями возможностей шифрования в соответствии с экспортными положениями US. Чтобы использовать неограниченные возможности шифрования - скачайте JCE (Java Crytographic Extensions). Посетите Sun's Java website.

Я установил JDK1.4.1 в /opt и переменной JAVA_HOME дал значение местонахождения JDK. Также сразу обновил PATH. Следующие строки я добавил в файл .bash_profile.

JAVA_HOME = /opt/j2sdk1.4.1_01
PATH = /opt/j2sdk1.4.1_01/bin:$PATH
export JAVA_HOME PATH

Стандартные(ограниченые) файлы для шифрования, входящие в состав Sun JDK, были заменены файлами из JCE. Чтобы java нашла JDBC драйверы для postgres я скопировал их в /opt/j2sdk1.4.1_01/jre/lib/ext. В дистрибутиве redhat 8.0 postgres-jdbc драйверы находятся в /usr/share/pgsql.

Если вы первый раз установили postgresql - вам необходимо создать новую базу и завести пользователя. Во-первых переключитесь в su и запустите сервис postgres. Затем измените настройки администратора postgres.

su root
password:******
[root#localhost]#/etc/init.d/postgresql start
[root#localhost]# Starting postgresql service: [ OK ]
[root#localhost]# su postgres
[bash]$

Создаем нового пользователя postgres и базу данных.

[bash]$:createuser
Enter name of user to add: chianglin
Shall the new user be allowed to create databases? (y/n) y
Shall the new user be allowed to create more new users? (y/n) y
CREATE USER
[bash]$createdb chianglin
CREATE DATABASE

Я создал нового администратора postgres в соответствии с моим пользовательским профилем в системе и новую базу данных с таким же именем. По умолчанию, когда вы запускаете psql, соединение происходит с базой соответствующей пользователю Linux. Подробнее читайте об этом в документации к postgres. Чтобы установить пароль новому пользователю - запустите psql и используйте команду ALTER USER. Наберите следующую команду

ALTER USER chianglin WITH PASSWORD 'test1234' ;

Чтобы разрешить tcp/ip соединения - отредактируйте postgresql.conf и установите опцию tcpip_socket в true. В redhat 8, этот файл расположен в /var/lib/pgsql/data. Переключитесь в root и установите опцию

tcpip_socket=true

И наконец отредактируйте файл pg_hba.conf. В нем определены компьютеры, имеющие доступ к базе данных postgres. Я добавил один компьютер - свой и установил использование пароля для него. И снова переключитесь в root, чтобы произвести необходимые изменения. 

host sameuser 127.0.0.1 255.255.255.255 password

Перезапустите postgres, чтобы новые установки вступили в силу.

Создание Java SSL Tunnel

Теперь у вас есть функционирующая postgres, готовая обслуживать небезопасные локальные JDBC соединения. Чтобы использовать безопасные удаленные запросы необходимо провести еще некоторые работы.

Следующая диграмма показывает как это будет работать.

Рисунок демонстрирует работу Java proxies

JDBC приложение устанавливает соединение с клиентским прокси, который будет направлять все данные через SSL соединение к удаленному серверному прокси. В свою очередь серверный прокси будет перенаправлять запросы к postgres и возвращать полученные данные JDBC приложению опять через SSL соединение. Весь этот процесс будет прозрачен для JDBC приложения.

Из диаграммы видно, что на стороне сервера необходимо сначала получить входящие данные и переслать их локально в базу и затем наоборот - сначала получить данные из базы и перенаправить их клиенту. То же самое применимо к клиентскому компьютеру. Для реализации этого будем использовать threads. Следующая диаграмма демонстрирует это.

Diagram showing how the 4 relaying threads work

Создание хранилищ ключей, ключей и сертификатов

При SSL соединении необходима серверная аутентификация, клиентская - по необходимости. Лично я предпочитаю использовать и клиентскую и серверную аутентификации, это значит, что я буду создавать ключи и сертификаты и для клиента и для сервера с помощью инструмента предоставляемого Java JDK.. В результате получится по два хранилища на каждой стороне - первое для хранения private ключа хоста и второе для хранения сертификата.

Серверное хранилище создается следующим образом - private ключ и public сертификат.

keytool -genkey -alias serverprivate -keystore servestore -keyalg rsa -keysize 2048

Enter keystore password: storepass1
What is your first and last name?
[Unknown]: ServerMachine
What is the name of your organizational unit?
[Unknown]: ServerOrg
What is the name of your organization?
[Unknown]: ServerOrg
What is the name of your City or Locality?
[Unknown]: Singapore
What is the name of your State or Province?
[Unknown]: Singapore
What is the two-letter country code for this unit?
[Unknown]: SG
Is CN=ServerMachine, OU=ServerOrg, O=ServerOrg, L=Singapore, ST=Singapore, C= [no]: yes
Enter key password for <serverprivate>
(RETURN if same as keystore password): prikeypass0 </serverprivate>

Обратите внимание, что пароли запрашиваются два раза - первый - для хранилища, а второй - для private ключа. После этого экспортируйте серверный сертификат в файл.

keytool -export -alias serverprivate -keystore -rfc servestore -file server.cer

Данной командой мы переместили серверный сертификат в файл server.cer. На клиентской стороне импортируйте этот файл в хранилище, где хранятся все сертификаты.

keytool -import -alias trustservercert -file server.cer -keystore clienttruststore

Предыдущая команда импортирует серверный сертификат в хранилище с названием clientruststore, если такого хранилища нет - оно будет создано и у вас попросят пароль для него. 

Теперь ваша система способна установить SSL соединение с серверной аутентификацией.
Но так как я решил использовать и клиентскую аутентификацию - необходимо создать private/public ключи в новом клиентском хранилище, экспортировать клиентский сертификат и импортировать его в серверное хранилище.

После этого мы получим два хранилища на сервере - одно для хранения private ключа и одно для сертификатов. То же самое и для клиента.

Чтобы использовать пример, рассматриваемый далее, необходимо установить одинаковые пароли для обоих хранилищ на компьютере - т.е. два хранилища на сервере должны иметь одинаковые пароли, то же самое и для клиентских хранилищ. 

Подробнее об использовании keytool смотрите тут -> Sun's documentation.

Классы

Мои классы будут использовать расширения Sun's Java Secured Socket. Документация по Sun JSSE доступна здесь http://java.sun.com/j2se/1.4.1/docs/guide/security/jsse/JSSERefGuide.html. Для использования ssl соединения необходимо создать экземпляр объекта SSLContext, предоставляемого JSSE. Инициализируйте объект SSLContext с необходимыми вам установками и создайте экземпляр класса Secured SocketFactory. socketfactory будет использоваться для создания ssl сокетов.

В моем случае будут созданы и клиентский и серверный классы для построения SSL тоннеля. Так как они оба будут использовать SSL соединение - они оба будут созданы на основе базового класса SSLConnection. Этот класс ответственнен за SSLContext, который будет использоваться и клиентскими и серверными прокси. И наконец, нам нужен еще класс для использования threads. Всего получается четыре класса.

Фрагмент класса SSLConnection

/* initKeyStore method to load the keystores which contain the private key and the trusted certificates */

public void initKeyStores(String key , String trust , char[] storepass)
{
      // mykey holding my own certificate and private key, mytrust holding all the certificates that I trust
  try {
      //get instances of the Sun JKS keystore
     mykey = KeyStore.getInstance("JKS" , "SUN");
     mytrust = KeyStore.getInstance("JKS", "SUN");

    //load the keystores
   mykey.load(new FileInputStream(key)  ,storepass);
   mytrust.load(new FileInputStream(trust) ,storepass );
    }
 catch(Exception e) {
    System.err.println(e.getMessage());
    System.exit(1);
    }
}

/* initSSLContext method to obtain a  SSLContext and initialize it with the SSL protocol and data from the keystores */
public void initSSLContext(char[] storepass , char[] keypass) {
    try{
    //get a SSLContext from Sun JSSE
    ctx = SSLContext.getInstance("TLSv1" , "SunJSSE") ;
    //initializes the keystores
    initKeyStores(key , trust , storepass) ;

    //Create the key and trust manager factories for handing the cerficates
    //in the key and trust stores
    TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509" ,
    "SunJSSE");
    tmf.init(mytrust);

    KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509" ,
    "SunJSSE");
    kmf.init(mykey , keypass);

    //initialize the SSLContext with the data from the keystores
    ctx.init(kmf.getKeyManagers() , tmf.getTrustManagers() ,null) ;
    }
    catch(Exception e) {
    System.err.println(e.getMessage());
    System.exit(1);
    }

}

Метод initSSLContext создает SSLContext из Sun JSSE. Во время создания необходимо указать используемый протокол SSL. Я выбрал TLS (Transport Layer Security) версии 1. После того как SSLContext получен - инициализируем его данными из хранилищ.


Следующий фрагмент из кода класса SSLRelayServer, который будет работать на том же компьютере, на котором работает postgres. Его функцией будет передача входящих данных от SSL соединения и передача их в postgres и наоборот.

Фрагмент класса SSLRelayServer

/* initSSLServerSocket method will get the SSLContext via its super class SSLConnection. It will then create a  SSLServerSocketFactory object that will be used to create a SSLServerSocket. */

public void initSSLServerSocket(int localport) {
      try{
           //get the ssl socket factory
           SSLServerSocketFactory ssf = (getMySSLContext()).getServerSocketFactory();

            //create the ssl socket
           ss = ssf.createServerSocket(localport);
           ((SSLServerSocket)ss).setNeedClientAuth(true);
      }
   catch(Exception e) {
      System.err.println(e.getMessage());
      System.exit(1);
    }
 }

// begin listening on SSLServerSocket and wait for incoming client connections
public void startListen(int localport , int destport) {

    System.out.println("SSLRelay server started at " + (new Date()) + "  " +
                     "listening on port " + localport + "  " +  "relaying to port " + destport );

 while(true) {
      try {
         SSLSocket incoming = (SSLSocket) ss.accept();
         incoming.setSoTimeout(10*60*1000); // set 10 minutes time out
         System.out.println((new Date() ) + " connection from " + incoming );
         createHandlers(incoming, destport); // create 2 new threads to handle the incoming connection
       }
    catch(IOException e ) {
        System.err.println(e);
        }
    }
}


Клиентский прокси RelayApp подобен SSLRelayServer. Он наследуется из SSLConnection и использует 2 threads для передачи данных. Разница в том, что он создает SSLSocket для соединения с удаленным компьютером вместо SSLServerSocket для прослушивания входящих соединений. И наконец, нам нужен класс для обеспечения передачи данных. Он просто читает данные из входящего потока и перенаправляет их в выходящий. 

Полностью четыре класса доступны здесь (example285-0.1.tar.gz).  

Тестирование proxies

На клиенте вам необходимы следующие файлы : SSLConnection.java, RelayIntoOut.java и RelayApp.java. На сервере : SSLRelayServer.java, RelayIntoOut.java и SSLConnection.java. Поместите их в один каталог. Скомпилируйте клиентский прокси :

javac RelayApp.java

Скомпилируйте серверный прокси :

javac SSLRelayServer.java

На сервере с postgres, запустите SSLRelayServer с 6-ю аргументами :

  1. Полный путь к хранилищу, хранящему серверный private ключ, созданный вами ранее с помощью keytool
  2. Полный путь к серверному хранилищу, хранящему сертификат
  3. Пароль для хранилищ
  4. Пароль для серверного private ключа
  5. Порт для прослушивания
  6. Порт, в который перенаправлять данные ( в нашем случае это порт 5432 по умолчанию используемый postgresql )

java SSLRelayServer servestore trustclientcert storepass1 prikeypass0 2001 5432

Запустив серверный прокси, вы можете запустить и клиентский. Он требует 7 аргументов, дополнительный - это имя или IP адрес сервера, к которому обращаться :

  1. Полный путь к хранилищу, хранящему клиентский private ключ
  2. Полный путь к клиентскому хранилищу, хранящему сертификат
  3. Пароль для хранилищ
  4. Пароль для клиентского private ключа
  5. Имя или IP адрес сервера
  6. Порт сервера ( в нашем случае 2001 )
  7. Порт приложения, в который перенаправлять данные ( в нашем случае это postgresql, следовательно порт 5432 )

java RelayApp clientstore trustservercert clistorepass1 cliprikeypass0 localhost 2001 5432

После того как SSL тоннель запущен - можете пользоваться JDBC приложением и работать с postgres как обычно. Весь процесс передачи данных будет прозрачным для вашего приложения. Заметка получилась достаточно большой и я не буду приводить здесь примеры JDBC приложений. В документации и postgres и sun достаточно таких примеров.

Если вы хотите все это протестировать на одном компьютере - вы это можете сделать либо поменяв порт для postgres, либо поменять порт RelayApp. Покажу вам как это сделать на втором варианте. Во-первых выйдите из RelayApp - пошлите ему сигнал [ctrl] c и таким же образом остановите SSLRelayServer прокси.

Запустите снова RelayApp следующей командой. Измените лишь номер последнего порта, теперь он 2002.

java RelayApp clientstore trustservercert clistorepass1 cliprikeypass0 localhost 2001 2002

Наилучшим приложением для тестирования будет psql. Мы будем перенаправлять весь psql трафик через наш тоннель. Запустите psql следующей командой :

psql -h localhost -p 2002

Команда перенаправляет psql соединение на порт 2002, который прослушивает RelayApp. После ввода вашего пароля postgres вы можете вводить SQL команды как обычно и тестировать SSL соединение, которое транслирует данные.

Безопасность

Не совсем правильно указывать пароли в командной строке, если вы не один используете компьютер. Любой пользователь вашего компьютера наберет команду ps -auxww и увидит все ваши процессы со всеми аргументами. Гораздо безопаснее хранить пароли в зашифрованном виде в другом файле и уже в java приложении брать их из этого файла. Также можно создать диалоговое окно, в котором требовать ввода паролей.

Вывод

Достаточно просто использовать Sun JSSE для создания   SSL тоннеля, который будет использовать и postgres. На самом деле любое приложение, требующее безопасного соединения, может использовать подобный SSL тоннель и также можно сказать, что существует множество путей добавить шифрование к вашему приложению - просто запустите ваш любимый текстовый редактор в Linux и кодируйте. Удачи !

Полезные ссылки