Many of you might have heard of the database credential rotation. It is a common solution for securing your database. Many providers have implemented this solution such as Hashicorp Vault, AWS Secret Manager, etc. By automatically rotating your database credential, no one can really know how to connect to the database. Indeed it is secure, but it leads to another problem. By rotating your database credential, your application might not be able to connect to the database after some times. Especially when you declare your database maximum connection lifetime. In this article, you will learn how to rotate your database credential programmatically in golang.
Custom Driver
To implement this rotator as a generic solution, you will need to create a customDriver struct which holds your base database driver (e.g postgres, sqlite3, mysql, etc) and a Fetcher interface which will be used to fetch the database credential.
typeFetcherinterface{Fetch()(string,error)}typeFetcherFuncfunc()(string,error)func(fFetcherFunc)Fetch()(string,error){returnf()}// customDriver implements the `sql.Driver` interface.typecustomDriverstruct{// base is the base database driverbasedriver.Driver// fetcherFn is the function that will be used to fetch the database credentialfetcherFnFetcherFunc}func(d*customDriver)Open(_string)(driver.Conn,error){// fetch the database credentialdsn,err:=d.fetcherFn()iferr!=nil{returnnil,err}// open the database connection using the fetched credential and base driverreturnd.base.Open(dsn)}
Register the Driver and Open the Connection
Now to be able to use the driver, you need to register the driver and open the connection.
1
2
3
4
5
6
7
8
9
funcOpenWithRotator(namestring,basedriver.Driver,fetcherFetcher)(*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.returnsql.Open(name,"")}
Implement Fetcher Interface
Let’s implement the Fetcher interface by using a simple function. Let’s assume that you want to connect to a different database for every time your database lifetime is up.
1
2
3
4
5
6
7
8
9
10
11
varcounterintfuncsimpleFetcher()(string,error){log.Println("fetcher called")// add your custom logic// e.g. fetching from vault / config / etc.counter++returnfmt.Sprintf("file:foobar-%d.sqlite",counter),nil}
Open the Connection with Rotator
Open and set the maximum lifetime of the connection to 2 seconds. simpleFetcher will be called every 2 seconds.
If your database credential is rotated faster than your connection lifetime, Golang still can use the old connection.
1
2
3
4
5
6
7
// you can adjust the `&sqlite3.SQLiteDriver{}` accordingly (e.g. &pq.Driver{}, etc.)db,err:=OpenWithRotator("foobar",&sqlite3.SQLiteDriver{},FetcherFunc(simpleFetcher))iferr!=nil{log.Fatal(err)}deferdb.Close()db.SetConnMaxLifetime(2*time.Second)
Test the Connection
To simply test whether the connection is working, you can use the Ping method every second.
Now when you run the program, you will see the following output:
1
2
3
4
5
6
7
$ 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
packagemainimport("database/sql""database/sql/driver""fmt""log""time""github.com/mattn/go-sqlite3")typeFetcherinterface{Fetch()(string,error)}typeFetcherFuncfunc()(string,error)func(fFetcherFunc)Fetch()(string,error){returnf()}// customDriver implements the `sql.Driver` interface.typecustomDriverstruct{// base is the base database driverbasedriver.Driver// fetcherFn is the function that will be used to fetch the database credentialfetcherFnFetcherFunc}func(d*customDriver)Open(_string)(driver.Conn,error){// fetch the database credentialdsn,err:=d.fetcherFn()iferr!=nil{returnnil,err}// open the database connection using the fetched credential and base driverreturnd.base.Open(dsn)}funcOpenWithRotator(namestring,basedriver.Driver,fetcherFetcher)(*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.returnsql.Open(name,"")}varcounterintfuncsimpleFetcher()(string,error){log.Println("fetcher called")// add your custom logic// e.g. fetching from vault / config / etc.counter++returnfmt.Sprintf("file:foobar-%d.sqlite",counter),nil}funcmain(){// you can adjust the `&sqlite3.SQLiteDriver{}` accordingly (e.g. &pq.Driver{}, etc.)db,err:=OpenWithRotator("foobar",&sqlite3.SQLiteDriver{},FetcherFunc(simpleFetcher))iferr!=nil{log.Fatal(err)}deferdb.Close()db.SetConnMaxLifetime(2*time.Second)forrangetime.Tick(time.Second){iferr:=db.Ping();err!=nil{log.Fatal(err)}}}
Conclusion
This is the simple implementation of a database rotator. You might want to implement your fetcher accordingly depends on your use case.
Thank you for reading!
···
Love This Content?
Any kind of supports is greatly appreciated! Kindly support me via Bitcoin, Ko-fi, Trakteer, or just continue to read another content. You can write a response via Webmention and let me know the URL via Telegraph.