Глубокое погружение в методы туннелирования SSH

В современном мире безопасность является главным приоритетом, особенно когда речь идет о конфиденциальных данных, хранящихся в базах данных. Одним из распространенных способов обеспечения безопасного доступа к удаленным базам данных является использование туннелей SSH. Этот метод устанавливает безопасное зашифрованное соединение между локальным компьютером и удаленным сервером, через которое маршрутизируется трафик базы данных.

В этой статье мы обсудим, как создать туннель SSH для безопасного доступа к базе данных с помощью Golang. Мы рассмотрим код реализации туннеля SSH и объясним, как он работает.

Что такое перенаправление портов

Переадресация портов, также известная как туннелирование SSH, — это функция SSH, которая позволяет пользователям безопасно получать доступ к сетевым службам, работающим в удаленной системе. При переадресации портов клиент SSH создает безопасный зашифрованный туннель между клиентом и сервером, позволяя пересылать трафик через туннель для доступа к ресурсам в удаленной системе.

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

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

Помимо обеспечения безопасного доступа к базе данных, переадресацию портов также можно использовать для безопасного доступа к другим сетевым службам, таким как веб-серверы, FTP-серверы и другие ресурсы в удаленной системе. С помощью переадресации портов SSH пользователи могут создать безопасный зашифрованный туннель для доступа к этим ресурсам, не открывая их в Интернете, что обеспечивает дополнительный уровень безопасности для сетевых служб.

Что мы будем строить

Позвольте мне представить диаграмму uml объекта, которую мы собираемся построить с помощью golang и его стандартной библиотеки.

На этой диаграмме показан процесс безопасного доступа к удаленной базе данных с использованием туннеля SSH, который мы собираемся реализовать в golang:

  • Клиент пытается подключиться к локальному прослушивателю, который является локальным прокси на его машине.
  • Локальный прослушиватель перенаправляет запрос на подключение клиенту SSH, который отвечает за установление безопасного подключения к удаленному серверу SSH.
  • Клиент SSH устанавливает зашифрованное соединение с сервером SSH, обеспечивая безопасный канал для передачи данных.
  • Сервер SSH перенаправляет запрос на подключение к удаленной базе данных.
  • Удаленная база данных отправляет данные обратно через сервер SSH.
  • Сервер SSH пересылает данные клиенту SSH по зашифрованному соединению.
  • Клиент SSH пересылает данные локальному прослушивателю.
  • Локальный прослушиватель отправляет данные обратно клиенту.

Этот процесс гарантирует, что данные, передаваемые между клиентом и удаленной базой данных, останутся в безопасности, поскольку они шифруются при прохождении через туннель SSH.

И файл puml, используемый для создания диаграммы.

@startuml

!define AWSPuml https://raw.githubusercontent.com/awslabs/aws-icons-for-plantuml/v15.0/dist

actor Client
participant "Local Listener" as LocalListener
participant "SSH Client" as SSHClient
participant "SSH Server" as SSHServer
database "Remote DB" as DB

Client -> LocalListener : Connect
activate LocalListener

LocalListener -> SSHClient : Forward connection
activate SSHClient

SSHClient -> SSHServer : Establish SSH connection
activate SSHServer

SSHServer -> DB : Forward connection
activate DB

DB --> SSHServer : Send data
SSHServer --> SSHClient : Forward data
SSHClient --> LocalListener : Forward data
LocalListener --> Client : Send data

deactivate LocalListener
deactivate SSHClient
deactivate SSHServer
deactivate DB

@enduml

Пример на ходу

Ниже приведен пример реализации с помощью go.

Настройка конфигурации туннеля SSH

Во-первых, нам нужно определить структуру для хранения необходимой конфигурации туннеля SSH. Структура SSHTunnelConfig содержит информацию о сервере SSH, аутентификации пользователя и удаленной базе данных, а также о экземплярах локального прослушивателя и клиента SSH.

// SSHTunnelConfig is a struct that holds the configuration for the SSH tunnel
type SSHTunnelConfig struct {
 SSHHost       string
 SSHPort       string
 SSHUser       string
 SSHKeyFile    string
 DbHost        string
 DbPort        string
 SSHClient     *ssh.Client
 LocalAddr     *net.TCPAddr
 LocalListener net.Listener
 Ctx           context.Context
 Cancel        context.CancelFunc
}

// New returns a new SSHTunnelConfig
func New(sshHost, sshPort, sshUser, sshKeyFile string) *SSHTunnelConfig {
 return &SSHTunnelConfig{
  SSHHost:    sshHost,
  SSHPort:    sshPort,
  SSHUser:    sshUser,
  SSHKeyFile: sshKeyFile,
 }
}

Установка туннеля SSH

Метод SetupTunnel устанавливает туннель SSH, выполняя следующие шаги:

  • Прочитайте файл закрытого ключа для аутентификации SSH.
  • Проанализируйте закрытый ключ, чтобы создать подписывающую сторону SSH. Это может быть файл .pem с закрытым ключом, используемый для аутентификации.
  • Настройте клиент SSH с аутентификацией пользователя и сведениями о сервере.
  • Подключиться к SSH-серверу.
  • Настройте переадресацию локального порта на случайный и свободный порт.
  • Создайте контекст с отменой, чтобы корректно закрыть SSHTunnel, например, прослушивая сигнал ОС о завершении программы, такой как Ctrl+C.
  • Начните пересылать соединения с помощью отдельной горутины.

Вот код метода SetupTunnel:

// SetupTunnel sets up the SSH tunnel
func (s *SSHTunnelConfig) SetupTunnel(dbHost, dbPort string) error {
 s.DbHost = dbHost
 s.DbPort = dbPort

 // Load the private key for SSH authentication
 key, err := os.ReadFile(s.SSHKeyFile)
 if err != nil {
  return fmt.Errorf("error reading private key: %v", err)
 }

 signer, err := ssh.ParsePrivateKey(key)
 if err != nil {
  return fmt.Errorf("error parsing private key: %v", err)
 }

 // Configure SSH client
 sshConfig := &ssh.ClientConfig{
  User: s.SSHUser,
  Auth: []ssh.AuthMethod{
   ssh.PublicKeys(signer),
  },
  HostKeyCallback: ssh.InsecureIgnoreHostKey(),
 }

 // Connect to the SSH Server
 sshConnection, err := ssh.Dial("tcp", s.SSHHost+":"+s.SSHPort, sshConfig)
 if err != nil {
  return fmt.Errorf("error dialing SSH server: %v", err)
 }
 s.SSHClient = sshConnection

 // Setup local port forwarding at random port
 localListener, err := net.Listen("tcp", "localhost:0")
 if err != nil {
  return fmt.Errorf("error setting up local port forwarding: %v", err)
 }
 s.LocalListener = localListener
 log.Printf("Local addrress to connect to db established: %v", localListener.Addr())
 s.LocalAddr = localListener.Addr().(*net.TCPAddr)

 // Create a context with cancellation
 s.Ctx, s.Cancel = context.WithCancel(context.Background())

 go s.forwardConnections()

 return nil

}

Интересная часть приведенного выше фрагмента кода, которая заслуживает некоторого дополнительного объяснения, это следующая:

localListener, err := net.Listen("tcp", "localhost:0")

:0 используется, чтобы позволить операционной системе автоматически выбирать доступный случайный порт для локального прослушивателя. Используя 0 в качестве номера порта, вы, по сути, говорите ОС найти свободный порт и привязать к нему слушателя. Это полезно, если вы не хотите жестко задавать конкретный номер порта или хотите избежать потенциальных конфликтов с другими службами, работающими на том же компьютере.

После настройки локального прослушивателя вы можете получить фактический номер порта, назначенный ОС, используя метод Addr() для объекта прослушивателя. В представленном коде это делается с помощью следующей строки:

s.LocalAddr = localListener.Addr().(*net.TCPAddr)

Это сохраняет адрес локального прослушивателя (включая выбранный номер порта) в поле LocalAddr структуры SSHTunnelConfig, поэтому вы можете позже использовать его для подключения к локальному прослушивателю.

Переадресация соединений

Метод forwardConnections принимает подключения от локального слушателя и перенаправляет их на удаленный сервер через туннель SSH:

  • Дождитесь подключения к локальному прослушивателю.
  • Подтвердите локальное подключение.
  • Наберите клиент SSH, чтобы установить соединение с удаленной базой данных через туннель SSH.
  • Используйте io.Copy для передачи данных между локальным и удаленным соединениями в обоих направлениях. Данные передаются между локальным и удаленным соединениями с помощью функции io.Copy в отдельных горутинах. Это позволяет одновременно передавать данные в обоих направлениях.

Вот код метода forwardConnections:

// forwardConnections forwards the local connections to the remote server
func (s *SSHTunnelConfig) forwardConnections() {
 for {
  select {
  // Exit if the context is cancelled
  case <-s.Ctx.Done():
   return

  // Accept local connections and forward them to the remote server
  default:

   log.Println("Waiting for local connection...")
   localConn, err := s.LocalListener.Accept()
   if err != nil {
    if os.IsTimeout(err) {
     log.Printf("Error accepting local connection: %v", err)
    } else {
     return
    }
    continue
   }

   log.Printf("Local connection accepted, connecting to db via SSH Server. %v", net.JoinHostPort(s.DbHost, s.DbPort))
   remoteConn, err := s.SSHClient.Dial("tcp", net.JoinHostPort(s.DbHost, s.DbPort))
   if err != nil {
    log.Printf("Error dialing remote connection: %v", err)
    continue
   }
   go func() {
    defer localConn.Close()
    defer remoteConn.Close()
    go io.Copy(localConn, remoteConn)
    io.Copy(remoteConn, localConn)
   }()
  }
 }
}

В нашем примере с golang компоненты на диаграмме в верхней части поста, соответствующие клиенту, локальному прослушивателю, SSH-клиенту и SSH-серверу, следующие:

  • Клиент: клиент напрямую не представлен в коде, так как это внешний объект, который подключается к локальному прослушивателю. Это может быть любое приложение, которому требуется доступ к удаленной базе данных через туннель SSH. Например, в целях тестирования я использовал oracle db для тестирования ssh-туннелирования.
 err = oracleDB.Connect(sshTunnel.LocalAddr.String())
 if err != nil {
  log.Fatalf("Error connecting to the Oracle database: %v", err)
 }
 defer oracleDB.DB.Close()

А затем в функции oracleDB.Connect я использовал локальный адрес, предоставленный sshTunnel:

func (config *OracleDBConfig) Connect(localAddress string) error {
 connectionString := fmt.Sprintf("oracle://%s:%s@%s/%s",
  config.User, config.Password, localAddress, config.SID)
 log.Printf("Opening connection to db: %v", connectionString)
 db, err := sql.Open("oracle", connectionString)
 if err != nil {
  return fmt.Errorf("error connecting to the Oracle database: %v", err)
 }
 config.DB = db
 return nil
}
  • Локальный прослушиватель. Локальный прослушиватель представлен полем LocalListener в структуре SSHTunnelConfig. Это net.Listener, который прослушивает входящие соединения по локальному адресу (localhost) и случайному порту. Локальный слушатель настроен в методе SetupTunnel:
localListener, err := net.Listen("tcp", "localhost:0")
  • Клиент SSH. Клиент SSH представлен полем SSHClient в структуре SSHTunnelConfig. Это экземпляр *ssh.Client, который создается путем подключения к SSH-серверу методом SetupTunnel:
sshConnection, err := ssh.Dial("tcp", s.SSHHost+":"+s.SSHPort, sshConfig)
  • Сервер SSH. Сервер SSH не представлен в коде напрямую как объект или переменная. Это удаленный сервер SSH, к которому подключается клиент SSH. Адрес SSH-сервера указан в полях SSHHost и SSHPort в структуре SSHTunnelConfig.

Когда клиент подключается к локальному слушателю, код принимает соединение в методе forwardConnections:

localConn, err := s.LocalListener.Accept()

После принятия локального подключения код вызывает SSH-клиент, чтобы установить подключение к удаленной базе данных через SSH-туннель:

remoteConn, err := s.SSHClient.Dial("tcp", net.JoinHostPort(s.DbHost, s.DbPort))

Наконец, данные передаются между локальным и удаленным соединениями с помощью функции io.Copy в отдельных горутинах. Это позволяет осуществлять одновременную передачу данных в обоих направлениях:

go io.Copy(localConn, remoteConn)
io.Copy(remoteConn, localConn)

Заключение

Благодаря этой реализации вы теперь можете безопасно получить доступ к удаленной базе данных, используя туннель SSH в Golang. Установив зашифрованное соединение между локальной машиной и удаленным сервером, вы можете защитить конфиденциальные данные от несанкционированного доступа и прослушивания.

Этот подход можно использовать с различными типами баз данных, если они поддерживают соединения TCP. Его также можно расширить для обработки дополнительных функций, таких как пул соединений, обработка ошибок и многое другое. Безопасный доступ к базе данных имеет решающее значение для защиты ваших данных, и туннелирование SSH — эффективный способ добиться этого в Golang.

Повышение уровня кодирования

Спасибо, что являетесь частью нашего сообщества! Перед тем, как ты уйдешь:

  • 👏 Хлопайте за историю и подписывайтесь на автора 👉
  • 📰 Смотрите больше контента в публикации Level Up Coding
  • 💰 Бесплатный курс собеседования по программированию ⇒ Просмотреть курс
  • 🔔 Подписывайтесь на нас: Twitter | ЛинкедИн | "Новостная рассылка"

🚀👉 Присоединяйтесь к коллективу талантов Level Up и найдите прекрасную работу