Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,14 @@ module github.com/scroll-tech/chain-monitor
go 1.19

require (
github.com/aws/aws-sdk-go-v2 v1.21.2
github.com/aws/aws-sdk-go-v2/config v1.18.45
github.com/aws/aws-sdk-go-v2/feature/rds/auth v1.2.21
github.com/bits-and-blooms/bitset v1.11.0
github.com/gin-contrib/pprof v1.4.0
github.com/gin-gonic/gin v1.9.1
github.com/go-resty/resty/v2 v2.10.0
github.com/jackc/pgx/v5 v5.4.3
github.com/mattn/go-colorable v0.1.13
github.com/mattn/go-isatty v0.0.19
github.com/pressly/goose/v3 v3.15.0
Expand All @@ -30,6 +34,16 @@ require (
github.com/Microsoft/hcsshim v0.11.1 // indirect
github.com/StackExchange/wmi v1.2.1 // indirect
github.com/VictoriaMetrics/fastcache v1.12.1 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.13.43 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.13 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.43 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.37 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.45 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.37 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.15.2 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.17.3 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.23.2 // indirect
github.com/aws/smithy-go v1.15.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/btcsuite/btcd v0.20.1-beta // indirect
github.com/bytedance/sonic v1.9.1 // indirect
Expand Down Expand Up @@ -75,7 +89,6 @@ require (
github.com/influxdata/line-protocol v0.0.0-20210311194329-9aa0e372d097 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
github.com/jackc/pgx/v5 v5.4.3 // indirect
github.com/jackpal/go-nat-pmp v1.0.2 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
Expand Down
30 changes: 30 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,32 @@ github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156 h1:eMwmnE/GDgah
github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156/go.mod h1:Cb/ax3seSYIx7SuZdm2G2xzfwmv3TPSk2ucNfQESPXM=
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
github.com/apache/arrow/go/arrow v0.0.0-20191024131854-af6fa24be0db/go.mod h1:VTxUBvSJ3s3eHAg65PNgrsn5BtqCRPdmyXh6rAfdxN0=
github.com/aws/aws-sdk-go-v2 v1.21.2 h1:+LXZ0sgo8quN9UOKXXzAWRT3FWd4NxeXWOZom9pE7GA=
github.com/aws/aws-sdk-go-v2 v1.21.2/go.mod h1:ErQhvNuEMhJjweavOYhxVkn2RUx7kQXVATHrjKtxIpM=
github.com/aws/aws-sdk-go-v2/config v1.18.45 h1:Aka9bI7n8ysuwPeFdm77nfbyHCAKQ3z9ghB3S/38zes=
github.com/aws/aws-sdk-go-v2/config v1.18.45/go.mod h1:ZwDUgFnQgsazQTnWfeLWk5GjeqTQTL8lMkoE1UXzxdE=
github.com/aws/aws-sdk-go-v2/credentials v1.13.43 h1:LU8vo40zBlo3R7bAvBVy/ku4nxGEyZe9N8MqAeFTzF8=
github.com/aws/aws-sdk-go-v2/credentials v1.13.43/go.mod h1:zWJBz1Yf1ZtX5NGax9ZdNjhhI4rgjfgsyk6vTY1yfVg=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.13 h1:PIktER+hwIG286DqXyvVENjgLTAwGgoeriLDD5C+YlQ=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.13/go.mod h1:f/Ib/qYjhV2/qdsf79H3QP/eRE4AkVyEf6sk7XfZ1tg=
github.com/aws/aws-sdk-go-v2/feature/rds/auth v1.2.21 h1:m/oetLggG4HFTcU0CkY1uR18uKRNTm+V1XocGd3Wcxk=
github.com/aws/aws-sdk-go-v2/feature/rds/auth v1.2.21/go.mod h1:XoCNC17AXoRDfkX2bsFsGsn036fch7ATgchnAy+PsOQ=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.43 h1:nFBQlGtkbPzp/NjZLuFxRqmT91rLJkgvsEQs68h962Y=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.43/go.mod h1:auo+PiyLl0n1l8A0e8RIeR8tOzYPfZZH/JNlrJ8igTQ=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.37 h1:JRVhO25+r3ar2mKGP7E0LDl8K9/G36gjlqca5iQbaqc=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.37/go.mod h1:Qe+2KtKml+FEsQF/DHmDV+xjtche/hwoF75EG4UlHW8=
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.45 h1:hze8YsjSh8Wl1rYa1CJpRmXP21BvOBuc76YhW0HsuQ4=
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.45/go.mod h1:lD5M20o09/LCuQ2mE62Mb/iSdSlCNuj6H5ci7tW7OsE=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.37 h1:WWZA/I2K4ptBS1kg0kV1JbBtG/umed0vwHRrmcr9z7k=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.37/go.mod h1:vBmDnwWXWxNPFRMmG2m/3MKOe+xEcMDo1tanpaWCcck=
github.com/aws/aws-sdk-go-v2/service/sso v1.15.2 h1:JuPGc7IkOP4AaqcZSIcyqLpFSqBWK32rM9+a1g6u73k=
github.com/aws/aws-sdk-go-v2/service/sso v1.15.2/go.mod h1:gsL4keucRCgW+xA85ALBpRFfdSLH4kHOVSnLMSuBECo=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.17.3 h1:HFiiRkf1SdaAmV3/BHOFZ9DjFynPHj8G/UIO1lQS+fk=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.17.3/go.mod h1:a7bHA82fyUXOm+ZSWKU6PIoBxrjSprdLoM8xPYvzYVg=
github.com/aws/aws-sdk-go-v2/service/sts v1.23.2 h1:0BkLfgeDjfZnZ+MhB3ONb01u9pwFYTCZVhlsSSBvlbU=
github.com/aws/aws-sdk-go-v2/service/sts v1.23.2/go.mod h1:Eows6e1uQEsc4ZaHANmsPRzAKcVDrcmjjWiih2+HUUQ=
github.com/aws/smithy-go v1.15.0 h1:PS/durmlzvAFpQHDs4wi4sNNP9ExsqZh6IlfdHXgKK8=
github.com/aws/smithy-go v1.15.0/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
Expand Down Expand Up @@ -210,6 +236,7 @@ github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
Expand Down Expand Up @@ -272,6 +299,8 @@ github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
Expand Down Expand Up @@ -808,6 +837,7 @@ gopkg.in/urfave/cli.v1 v1.20.0/go.mod h1:vuBzUtMdQeixQj8LVd+/98pzhxNGQoyuPBlsXHO
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
Expand Down
51 changes: 51 additions & 0 deletions internal/utils/database/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# database

Connection helpers for the chain-monitor Postgres database (GORM + pgx).

## AWS RDS IAM authentication

`InitDB` can authenticate to AWS RDS/Aurora PostgreSQL using short-lived IAM
auth tokens instead of a DSN password. This removes the need to rotate database
passwords: access is granted via IAM and tokens are regenerated automatically
(they expire every 15 minutes) for each new connection.

Enable it in the `db_config` block:

```json
{
"driver_name": "postgres",
"dsn": "postgres://svc_user@mydb.abc123.us-east-1.rds.amazonaws.com:5432/scroll?sslmode=require",
"maxOpenNum": 200,
"maxIdleNum": 20,
"useIAMAuth": true,
"awsRegion": "us-east-1"
}
```

RDS IAM tokens are signed per `(host, port, db-user)`: the DSN's DB user must be
granted `rds_iam`, and the workload's IAM role must have `rds-db:connect` on the
corresponding `dbuser` resource.

Notes:

- Omit the password from the `dsn`; it is supplied by the generated IAM token.
- `awsRegion` is optional — when empty it is resolved from the default AWS
config chain (e.g. the `AWS_REGION` environment variable). AWS credentials come
from the default chain too (env vars, EKS IRSA / EC2 instance role), which
self-refreshes.
- IAM auth requires TLS, so the DSN must set `sslmode=require` or higher.
`sslmode=disable`/`allow`/`prefer` (including an unset `sslmode`, which
defaults to `prefer`) are rejected at startup rather than silently sending the
token over a connection that may fall back to plaintext. `require` encrypts
but does not verify the server certificate; for that, use `sslmode=verify-full`
with `sslrootcert` pointing at the
[RDS CA bundle](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/UsingWithRDS.SSL.html).
- Leaving `useIAMAuth` unset preserves the previous password-based behavior.

### Metrics

When IAM auth is active the following Prometheus metrics are exported:

- `database_rds_iam_token_total` — tokens generated (one per new connection).
- `database_rds_iam_token_failure_total` — token generation failures.
- `database_rds_iam_token_duration_seconds` — token generation latency.
7 changes: 7 additions & 0 deletions internal/utils/database/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,11 @@ type Config struct {

MaxOpenNum int `json:"maxOpenNum"`
MaxIdleNum int `json:"maxIdleNum"`

// UseIAMAuth authenticates to AWS RDS/Aurora with short-lived IAM tokens
// instead of a DSN password. The DSN should omit the password and enable TLS.
UseIAMAuth bool `json:"useIAMAuth"`
// AWSRegion signs the IAM tokens. Optional; falls back to the default AWS
// config chain (e.g. AWS_REGION) when empty.
AWSRegion string `json:"awsRegion"`
}
29 changes: 28 additions & 1 deletion internal/utils/database/db.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,12 @@ func InitDB(config *Config) (*gorm.DB, error) {
tmpGormLogger := gormLogger{
gethLogger: log.Root(),
}
db, err := gorm.Open(postgres.Open(config.DSN), &gorm.Config{
dialector, err := newDialector(config)
if err != nil {
return nil, err
}

db, err := gorm.Open(dialector, &gorm.Config{
Logger: &tmpGormLogger,
NowFunc: func() time.Time {
// why set time to UTC.
Expand All @@ -69,12 +74,34 @@ func InitDB(config *Config) (*gorm.DB, error) {
return nil, pingErr
}

if config.UseIAMAuth {
// IAM tokens expire after 15 minutes and a fresh one is minted per new
// connection, so recycle connections well within that window.
sqlDB.SetConnMaxLifetime(time.Minute * 10)
sqlDB.SetConnMaxIdleTime(time.Minute * 5)
}

sqlDB.SetMaxOpenConns(config.MaxOpenNum)
sqlDB.SetMaxIdleConns(config.MaxIdleNum)

return db, nil
}

// newDialector builds the gorm postgres dialector. With IAM auth it wraps a
// token-refreshing connector in a *sql.DB; otherwise it opens the DSN directly.
func newDialector(config *Config) (gorm.Dialector, error) {
if !config.UseIAMAuth {
return postgres.Open(config.DSN), nil
}

log.Info("connecting to database with AWS RDS IAM auth", "region", config.AWSRegion)
connector, err := NewRDSIAMConnector(context.Background(), config.DSN, config.AWSRegion)
if err != nil {
return nil, err
}
return postgres.New(postgres.Config{Conn: sql.OpenDB(connector)}), nil
}

// CloseDB close the db handler. notice the db handler only can close when then program exit.
func CloseDB(db *gorm.DB) error {
sqlDB, err := db.DB()
Expand Down
104 changes: 104 additions & 0 deletions internal/utils/database/iam.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package database

import (
"context"
"database/sql/driver"
"fmt"
"net"
"strconv"
"time"

"github.com/aws/aws-sdk-go-v2/aws"
awsconfig "github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/feature/rds/auth"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/stdlib"
)

// rdsIAMConnector is a driver.Connector that mints a fresh AWS RDS IAM auth
// token for every new connection (tokens expire after 15 minutes; already-open
// connections keep working since the token is only checked at connect time).
type rdsIAMConnector struct {
base *pgx.ConnConfig // parsed DSN; Password is set to the token per-connect
endpoint string // host:port the token is signed for
region string // AWS region the token is signed for
creds aws.CredentialsProvider // refreshes credentials internally
metrics *rdsIAMMetrics
}

// NewRDSIAMConnector returns a connector that injects a fresh RDS IAM token as
// the password on each new connection. region falls back to the default AWS
// config chain (e.g. AWS_REGION) when empty.
//
// IAM auth requires TLS, so a DSN with sslmode=disable is rejected rather than
// silently downgraded; sslmode=verify-full (+ sslrootcert) is recommended.
func NewRDSIAMConnector(ctx context.Context, dsn, region string) (driver.Connector, error) {
base, err := pgx.ParseConfig(dsn)
if err != nil {
return nil, fmt.Errorf("rds iam: parse dsn: %w", err)
}
if base.User == "" {
return nil, fmt.Errorf("rds iam: dsn must specify a database user")
}

var opts []func(*awsconfig.LoadOptions) error
if region != "" {
opts = append(opts, awsconfig.WithRegion(region))
}
awsCfg, err := awsconfig.LoadDefaultConfig(ctx, opts...)
if err != nil {
return nil, fmt.Errorf("rds iam: load aws config: %w", err)
}
if region == "" {
region = awsCfg.Region
}
if region == "" {
return nil, fmt.Errorf("rds iam: aws region is not set (configure awsRegion or AWS_REGION)")
}

// Fail closed on any path that could send the token (which is the password)
// in cleartext or to the wrong host. The token is scoped to a single
// host:port, so reject:
// - TLS-less DSNs: sslmode=disable/allow leave the primary TLSConfig nil;
// - plaintext fallbacks: sslmode=prefer (the pgx default) keeps a non-TLS
// entry in Fallbacks that pgx retries on if SSL negotiation fails;
// - multi-host DSNs: a fallback host the single token is not signed for.
if base.TLSConfig == nil {
return nil, fmt.Errorf("rds iam: TLS is required, set sslmode=require or higher (verify-full recommended) in the dsn")
}
for _, fb := range base.Fallbacks {
if fb.TLSConfig == nil {
return nil, fmt.Errorf("rds iam: dsn allows a plaintext fallback (sslmode=prefer); set sslmode=require or higher")
}
if fb.Host != base.Host || fb.Port != base.Port {
return nil, fmt.Errorf("rds iam: multi-host dsn is not supported; the IAM token is scoped to a single host:port")
}
}

return &rdsIAMConnector{
base: base,
endpoint: net.JoinHostPort(base.Host, strconv.Itoa(int(base.Port))),
region: region,
creds: awsCfg.Credentials,
metrics: initRDSIAMMetrics(),
}, nil
}

// Connect generates a fresh IAM auth token and opens a new connection with it.
func (c *rdsIAMConnector) Connect(ctx context.Context) (driver.Conn, error) {
start := time.Now()
token, err := auth.BuildAuthToken(ctx, c.endpoint, c.region, c.base.User, c.creds)
c.metrics.tokenDuration.Observe(time.Since(start).Seconds())
if err != nil {
c.metrics.tokenFailureTotal.Inc()
return nil, fmt.Errorf("rds iam: build auth token: %w", err)
}
c.metrics.tokenTotal.Inc()

cfg := c.base.Copy()
cfg.Password = token
return stdlib.GetConnector(*cfg).Connect(ctx)
}

// Driver returns the underlying pgx stdlib driver.
func (c *rdsIAMConnector) Driver() driver.Driver { return stdlib.GetDefaultDriver() }
41 changes: 41 additions & 0 deletions internal/utils/database/iam_metrics.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package database

import (
"sync"

"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
)

type rdsIAMMetrics struct {
tokenTotal prometheus.Counter
tokenFailureTotal prometheus.Counter
tokenDuration prometheus.Histogram
}

var (
initRDSIAMMetricsOnce sync.Once
rdsIAMMetric *rdsIAMMetrics
)

func initRDSIAMMetrics() *rdsIAMMetrics {
initRDSIAMMetricsOnce.Do(func() {
reg := prometheus.DefaultRegisterer
rdsIAMMetric = &rdsIAMMetrics{
tokenTotal: promauto.With(reg).NewCounter(prometheus.CounterOpts{
Name: "database_rds_iam_token_total",
Help: "Total number of AWS RDS IAM auth tokens generated (one per new connection).",
}),
tokenFailureTotal: promauto.With(reg).NewCounter(prometheus.CounterOpts{
Name: "database_rds_iam_token_failure_total",
Help: "Total number of AWS RDS IAM auth token generation failures.",
}),
tokenDuration: promauto.With(reg).NewHistogram(prometheus.HistogramOpts{
Name: "database_rds_iam_token_duration_seconds",
Help: "Latency of AWS RDS IAM auth token generation; spikes indicate a credential refresh.",
Buckets: []float64{.0001, .001, .005, .01, .05, .1, .5, 1, 5},
}),
}
})
return rdsIAMMetric
}
Loading
Loading