Обработка ротации учетных данных в базе данных в Golang

Введение

Многие из вас, возможно, слышали о ротации учетных данных в базе данных. Это распространенное решение для защиты вашей базы данных. Многие поставщики реализовали это решение, например, Hashicorp Vault, AWS Secret Manager и т.д. Благодаря автоматической ротации учетных данных базы данных никто не может знать, как подключиться к базе данных. Действительно, это безопасно, но это приводит к другой проблеме. При ротации учетных данных базы данных ваше приложение может не иметь возможности подключиться к базе данных через некоторое время. Особенно если вы объявите максимальное время жизни соединения с базой данных. В этой статье вы узнаете, как программно ротировать учетные данные базы данных в golang.

Пользовательский драйвер

Чтобы реализовать этот ротатор как общее решение, вам нужно создать customDriver struct, который содержит ваш базовый драйвер базы данных (например, postgres, sqlite3, mysql и т.д.) и Fetcher interface, который будет использоваться для получения учетных данных базы данных.

type Fetcher interface {
    Fetch() (string, error)
}

type FetcherFunc func() (string, error)
func (f FetcherFunc) Fetch() (string, error) {
    return f()
}

// customDriver implements the `sql.Driver` interface.
type customDriver struct {
    // base is the base database driver
    base      driver.Driver

    // fetcherFn is the function that will be used to fetch the database credential
    fetcherFn FetcherFunc
}

func (d *customDriver) Open(_ string) (driver.Conn, error) {
    // fetch the database credential
    dsn, err := d.fetcherFn()
    if err != nil {
        return nil, err
    }

    // open the database connection using the fetched credential and base driver
    return d.base.Open(dsn)
}
Вход в полноэкранный режим Выход из полноэкранного режима

Зарегистрируйте драйвер и откройте соединение

Теперь, чтобы иметь возможность использовать драйвер, необходимо зарегистрировать драйвер и открыть соединение.

func OpenWithRotator(name string, base driver.Driver, fetcher Fetcher) (*sql.DB, error) {
    sql.Register(name, &customDriver{
        base:      base,
        fetcherFn: fetcher.Fetch,
    })

    // you don't need to fill the dsn, it will be fetched from the fetcher.
    return sql.Open(name, "")
}
Войти в полноэкранный режим Выход из полноэкранного режима

Реализация интерфейса фетчера

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

var counter int

func simpleFetcher() (string, error) {
    log.Println("fetcher called")

    // add your custom logic
    // e.g. fetching from vault / config / etc.

    counter++
    return fmt.Sprintf("file:foobar-%d.sqlite", counter), nil
}
Вход в полноэкранный режим Выйдите из полноэкранного режима

Открыть соединение с помощью ротатора

Откройте и установите максимальное время жизни соединения на 2 секунды. simpleFetcher будет вызываться каждые 2 секунды.

Если ротация учетных данных вашей базы данных происходит быстрее, чем время жизни соединения, Golang все еще может использовать старое соединение.

// you can adjust the `&sqlite3.SQLiteDriver{}` accordingly (e.g. &pq.Driver{}, etc.)
db, err := OpenWithRotator("foobar", &sqlite3.SQLiteDriver{}, FetcherFunc(simpleFetcher))
if err != nil {
    log.Fatal(err)
}
defer db.Close()
db.SetConnMaxLifetime(2 * time.Second)
Вход в полноэкранный режим Выход из полноэкранного режима

Проверка соединения

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

for range time.Tick(time.Second) {
    if err := db.Ping(); err != nil {
        log.Fatal(err)
    }
}
Войти в полноэкранный режим Выйти из полноэкранного режима

Теперь, когда вы запустите программу, вы увидите следующий результат:

$ go run .
2022/08/29 16:56:16 fetcher called
2022/08/29 16:56:19 fetcher called
2022/08/29 16:56:22 fetcher called
2022/08/29 16:56:25 fetcher called
2022/08/29 16:56:28 fetcher called
2022/08/29 16:56:31 fetcher called
Вход в полноэкранный режим Выход из полноэкранного режима

Вот файлы базы данных sqlite3:

$ ls | grep foobar-
foobar-1.sqlite
foobar-2.sqlite
foobar-3.sqlite
foobar-4.sqlite
foobar-5.sqlite
foobar-6.sqlite
Войти в полноэкранный режим Выход из полноэкранного режима

Вот полный код:

package main

import (
    "database/sql"
    "database/sql/driver"
    "fmt"
    "log"
    "time"

    "github.com/mattn/go-sqlite3"
)

type Fetcher interface {
    Fetch() (string, error)
}

type FetcherFunc func() (string, error)
func (f FetcherFunc) Fetch() (string, error) {
    return f()
}

// customDriver implements the `sql.Driver` interface.
type customDriver struct {
    // base is the base database driver
    base      driver.Driver

    // fetcherFn is the function that will be used to fetch the database credential
    fetcherFn FetcherFunc
}

func (d *customDriver) Open(_ string) (driver.Conn, error) {
    // fetch the database credential
    dsn, err := d.fetcherFn()
    if err != nil {
        return nil, err
    }

    // open the database connection using the fetched credential and base driver
    return d.base.Open(dsn)
}

func OpenWithRotator(name string, base driver.Driver, fetcher Fetcher) (*sql.DB, error) {
    sql.Register(name, &customDriver{
        base:      base,
        fetcherFn: fetcher.Fetch,
    })

    // you don't need to fill the dsn, it will be fetched from the fetcher.
    return sql.Open(name, "")
}

var counter int
func simpleFetcher() (string, error) {
    log.Println("fetcher called")

    // add your custom logic
    // e.g. fetching from vault / config / etc.

    counter++
    return fmt.Sprintf("file:foobar-%d.sqlite", counter), nil
}

func main() {
    // you can adjust the `&sqlite3.SQLiteDriver{}` accordingly (e.g. &pq.Driver{}, etc.)
    db, err := OpenWithRotator("foobar", &sqlite3.SQLiteDriver{}, FetcherFunc(simpleFetcher))
    if err != nil {
        log.Fatal(err)
    }
    defer db.Close()
    db.SetConnMaxLifetime(2 * time.Second)

    for range time.Tick(time.Second) {
        if err := db.Ping(); err != nil {
            log.Fatal(err)
        }
    }
}
Войти в полноэкранный режим Выход из полноэкранного режима

Заключение

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

Спасибо за прочтение!

Оцените статью
devanswers.ru
Добавить комментарий