Глубокое погружение в методы туннелирования 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 и найдите прекрасную работу