新连接失败时,使用数据库/ SQL库并从保管库获取密码

新连接失败时,使用数据库/ SQL库并从保管库获取密码

问题描述:

I have a long running daemon written in Go, that listens to a port and spins up multiple go routines for every new connection to handle the data. There is a global variable db that is assigned connection context returned by database/sql library's open() function in the func main() of my script.

We store the db password in a vault which rotates it every couple of days for security reasons. I can fetch the password from the vault the first time it creates the connection context and the same context is used in all go routines for creating new db connections. However, when the password is rotated by the vault, all new db connections fail. I would like to know what is the best way of handling this so that it fetches the password from the vault on failure and reconnects. If it was an oop language, I could extend the db library and override the connection function to catch the error and fetch password from the vault on connection failure. Is there a similar approach that can be used in Go or are there any other way of handling this? I am very new to Go, and apologies if I have not framed the question right.

package main

import (
    "database/sql"
    _ "github.com/go-sql-driver/mysql"
    "net"
)

var db *sql.DB
const port = "port number"

func main() {

    db, err = sql.Open("mysql","<Connection string that contains the password fetched from vault>")

    db.SetMaxOpenConns(100)

    listener, err := net.Listen("tcp", ":"+port)

    for {
        conn, err := listener.Accept() 

        go handleConnection(conn)
    }
}

func handleConnection(conn net.Conn) {
    // Uses db variable to connect to db.
}

I have solved this problem by creating a custom driver that imports github.com/go-sql-driver/mysql and get registered in the sql package, so that when ever a new connection is required the custome driver can fetch password from vault and pass it down to mysql driver to open the connection. Refer the code sample below.

package vault-mysql-driver

import (
    "database/sql"
    "github.com/go-sql-driver/mysql"
)

type VaultMysqlDriver struct {
    *mysql.MySQLDriver
}

func updateDsn(dsn string) (string, err) {
    // logic to fetch password from vault and update dsn with the password
}

func (d VaultMysqlDriver) Open(dsn string) (driver.Conn, error) {

    updateddsn, err := updateDsn(dsn)

    // Pass down the dsn with password to mysql driver's open function
    return d.MySQLDriver.Open(updateddsn)

}

// When initialised will register the driver in sql package
func init() {
    sql.Register(vault-driver, &CyberarkMysqlDriver{&mysql.MySQLDriver{}})
}

This package is now imported in the daemon as below,

import (
    "database/sql"
    _ "vault-mysql-driver"// init is invoked and it will get registered in sql package
    "net"
)

var db *sql.DB
const port = "port number"

func main() {
    // vault-driver is used instead of mysql so that the sql package knows to use the custom driver for new connections.
    db, err = sql.Open("vault-driver","<Connection string that contains the password fetched from vault>")

    db.SetMaxOpenConns(100)

    listener, err := net.Listen("tcp", ":"+port)

    for {
        conn, err := listener.Accept() 

        go handleConnection(conn)
    }
}

func handleConnection(conn net.Conn) {
    // Uses db variable to connect to db.
}

This way whenever the vault rotates the password there won't be any connection failure since the vault driver will always fetch password from vault for every new connections.

Why do you have to create context on main? why not create a separate package/namespace and perform db connections in there. create a new namespace with methods... createDBClient() this creates new db contexts by fetching password from vault and stores it in global var db GetDBClient() this checks if the global var db has something in it, if so check if it can connect to db (https://golang.org/pkg/database/sql/#DB.Ping) if not call createDBClient() loop this for few times with sleep if doesnt connect crash and burn if it connects return the context

now in your handleConnection() call db := newnamespace.GetDBClient()