From 1f71bc10364e098dd8a0143b3380a43e36ba7cbf Mon Sep 17 00:00:00 2001 From: Anton Filonenko Date: Fri, 8 May 2026 15:27:40 +0300 Subject: [PATCH 1/2] MF-M05: fix(nitronode): enforce TLS by default for Postgres MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the hardcoded `sslmode=disable` in postgresqlDbUrl with a configurable NITRONODE_DATABASE_SSLMODE (default `require`), validate the mode against the libpq list, and honor NITRONODE_DATABASE_URL verbatim when set so operators can supply a fully-qualified DSN. Wire the helm chart to propagate `config.database.sslmode` through NITRONODE_DATABASE_SSLMODE; chart default stays `disable` to avoid breaking existing deployments — operators must opt-in to TLS via values. Co-Authored-By: Claude Opus 4.7 (1M context) --- nitronode/README.md | 3 +- nitronode/chart/templates/helpers/_common.tpl | 2 + nitronode/store/database/database.go | 33 +++++++- nitronode/store/database/database_test.go | 77 +++++++++++++++++++ 4 files changed, 112 insertions(+), 3 deletions(-) create mode 100644 nitronode/store/database/database_test.go diff --git a/nitronode/README.md b/nitronode/README.md index 88481b618..fa6b520c3 100644 --- a/nitronode/README.md +++ b/nitronode/README.md @@ -85,7 +85,8 @@ assets: |----------|-------------|---------| | `NITRONODE_SIGNER_KEY` | Private key for signing node state updates | (Required) | | `NITRONODE_DATABASE_DRIVER` | `sqlite` or `postgres` | `sqlite` | -| `NITRONODE_DATABASE_URL` | Connection string or file path | `nitronode.db` | +| `NITRONODE_DATABASE_URL` | Postgres DSN/URL or sqlite file path. When set for `postgres`, used verbatim and overrides the individual host/user/password/sslmode fields | `nitronode.db` | +| `NITRONODE_DATABASE_SSLMODE` | Postgres SSL mode: `disable`, `allow`, `prefer`, `require`, `verify-ca`, `verify-full` | `require` | | `NITRONODE_LOG_LEVEL` | `debug`, `info`, `warn`, `error` | `info` | | `NITRONODE_BLOCKCHAIN_RPC_` | RPC endpoint for a specific blockchain | (Required) | diff --git a/nitronode/chart/templates/helpers/_common.tpl b/nitronode/chart/templates/helpers/_common.tpl index 6848628cb..b648414ae 100644 --- a/nitronode/chart/templates/helpers/_common.tpl +++ b/nitronode/chart/templates/helpers/_common.tpl @@ -87,6 +87,8 @@ Returns common environment variables value: "{{ print .port }}" - name: NITRONODE_DATABASE_USERNAME value: {{ .user }} +- name: NITRONODE_DATABASE_SSLMODE + value: {{ .sslmode | default "disable" | quote }} {{- end }} {{- range $key, $value := .Values.config.extraEnvs }} - name: {{ $key | upper }} diff --git a/nitronode/store/database/database.go b/nitronode/store/database/database.go index f58601d90..2085c46c4 100644 --- a/nitronode/store/database/database.go +++ b/nitronode/store/database/database.go @@ -19,6 +19,9 @@ import ( // // To connect to sqlite, you just need to specify "sqlite" driver. // By default it will use in-memory database. You can provide NITRONODE_DATABASE_NAME to use the file. +// +// For Postgresql, NITRONODE_DATABASE_URL takes precedence: when set, it is used verbatim +// and the individual Username/Password/Host/Port/Name/SSLMode fields are ignored. type DatabaseConfig struct { URL string `env:"NITRONODE_DATABASE_URL" env-default:""` Name string `env:"NITRONODE_DATABASE_NAME" env-default:""` @@ -28,6 +31,7 @@ type DatabaseConfig struct { Password string `env:"NITRONODE_DATABASE_PASSWORD" env-default:"your-super-secret-and-long-postgres-password"` Host string `env:"NITRONODE_DATABASE_HOST" env-default:"localhost"` Port string `env:"NITRONODE_DATABASE_PORT" env-default:"5432"` + SSLMode string `env:"NITRONODE_DATABASE_SSLMODE" env-default:"require"` Retries int `env:"NITRONODE_DATABASE_RETRIES" env-default:"5"` // Connection pool settings @@ -127,12 +131,37 @@ func connectToSqlite(cnf DatabaseConfig) (*gorm.DB, error) { return db, nil } +// validPostgresSSLModes lists sslmode values accepted by libpq / pgx. +// See https://www.postgresql.org/docs/current/libpq-ssl.html. +var validPostgresSSLModes = map[string]struct{}{ + "disable": {}, + "allow": {}, + "prefer": {}, + "require": {}, + "verify-ca": {}, + "verify-full": {}, +} + func postgresqlDbUrl(cnf DatabaseConfig) (string, error) { switch cnf.Driver { case "postgres": + // URL, when supplied, is used verbatim. The operator owns sslmode, search_path, + // and any other parameters encoded in it. + if cnf.URL != "" { + return cnf.URL, nil + } + + sslMode := cnf.SSLMode + if sslMode == "" { + sslMode = "require" + } + if _, ok := validPostgresSSLModes[sslMode]; !ok { + return "", fmt.Errorf("invalid sslmode %q: must be one of disable, allow, prefer, require, verify-ca, verify-full", sslMode) + } + dsn := fmt.Sprintf( - "user=%s password=%s host=%s port=%s dbname=%s sslmode=disable", - cnf.Username, cnf.Password, cnf.Host, cnf.Port, cnf.Name, + "user=%s password=%s host=%s port=%s dbname=%s sslmode=%s", + cnf.Username, cnf.Password, cnf.Host, cnf.Port, cnf.Name, sslMode, ) if cnf.Schema != "" { diff --git a/nitronode/store/database/database_test.go b/nitronode/store/database/database_test.go new file mode 100644 index 000000000..5cdc5c784 --- /dev/null +++ b/nitronode/store/database/database_test.go @@ -0,0 +1,77 @@ +package database + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestPostgresqlDbUrl(t *testing.T) { + base := DatabaseConfig{ + Driver: "postgres", + Username: "user", + Password: "pass", + Host: "db.example.com", + Port: "5432", + Name: "nitronode", + } + + t.Run("DefaultsToRequireWhenSSLModeEmpty", func(t *testing.T) { + dsn, err := postgresqlDbUrl(base) + require.NoError(t, err) + assert.Contains(t, dsn, "sslmode=require") + assert.NotContains(t, dsn, "sslmode=disable") + }) + + t.Run("HonorsExplicitSSLMode", func(t *testing.T) { + cnf := base + cnf.SSLMode = "verify-full" + dsn, err := postgresqlDbUrl(cnf) + require.NoError(t, err) + assert.Contains(t, dsn, "sslmode=verify-full") + }) + + t.Run("AllowsDisableForLocalDev", func(t *testing.T) { + cnf := base + cnf.SSLMode = "disable" + dsn, err := postgresqlDbUrl(cnf) + require.NoError(t, err) + assert.Contains(t, dsn, "sslmode=disable") + }) + + t.Run("RejectsInvalidSSLMode", func(t *testing.T) { + cnf := base + cnf.SSLMode = "totally-bogus" + _, err := postgresqlDbUrl(cnf) + require.Error(t, err) + assert.Contains(t, err.Error(), "invalid sslmode") + }) + + t.Run("AppendsSearchPathWhenSchemaSet", func(t *testing.T) { + cnf := base + cnf.Schema = "tenant_a" + dsn, err := postgresqlDbUrl(cnf) + require.NoError(t, err) + assert.Contains(t, dsn, "search_path=tenant_a") + }) + + t.Run("URLOverridesIndividualFields", func(t *testing.T) { + cnf := base + cnf.URL = "postgres://override:secret@otherhost:6543/otherdb?sslmode=verify-ca" + cnf.SSLMode = "disable" // ignored when URL set + dsn, err := postgresqlDbUrl(cnf) + require.NoError(t, err) + assert.Equal(t, cnf.URL, dsn) + assert.False(t, strings.Contains(dsn, "user=user"), "URL must be returned verbatim") + }) + + t.Run("RejectsUnsupportedDriver", func(t *testing.T) { + cnf := base + cnf.Driver = "mysql" + _, err := postgresqlDbUrl(cnf) + require.Error(t, err) + assert.Contains(t, err.Error(), "unsupported driver") + }) +} From 3fc1c86ffe1b769afba58ee72d79917b3c9c1ede Mon Sep 17 00:00:00 2001 From: Anton Filonenko Date: Mon, 11 May 2026 16:44:26 +0300 Subject: [PATCH 2/2] MF-M05: fix(nitronode/chart): default sslmode to require Flip the helm chart's `config.database.sslmode` default from `disable` to `require` so deployments that expose Postgres over an untrusted network get TLS without extra configuration, matching the binary default. Setups where Postgres is only reachable on a private network (e.g. an in-cluster pgbouncer or VPC-only Cloud SQL) can opt out by setting `sslmode: disable`. The stress-v1 profile does so explicitly, since its pgbouncer is cluster-internal. Co-Authored-By: Claude Opus 4.7 (1M context) --- nitronode/chart/config/stress-v1/nitronode.yaml.gotmpl | 3 +++ nitronode/chart/templates/helpers/_common.tpl | 2 +- nitronode/chart/values.yaml | 9 +++++++-- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/nitronode/chart/config/stress-v1/nitronode.yaml.gotmpl b/nitronode/chart/config/stress-v1/nitronode.yaml.gotmpl index 30e93161a..0ec6bf54e 100644 --- a/nitronode/chart/config/stress-v1/nitronode.yaml.gotmpl +++ b/nitronode/chart/config/stress-v1/nitronode.yaml.gotmpl @@ -9,6 +9,9 @@ config: port: 5432 name: nitronode_stress_v1 user: nitronode_stress_v1_admin + # pgbouncer runs in-cluster on a private network, so TLS is not required + # here. Override the chart's `require` default explicitly. + sslmode: disable envSecret: nitronode-secret-env extraEnvs: NITRONODE_DATABASE_MAX_OPEN_CONNS: "{{ $p.database.maxOpenConns }}" diff --git a/nitronode/chart/templates/helpers/_common.tpl b/nitronode/chart/templates/helpers/_common.tpl index b648414ae..ed39a50db 100644 --- a/nitronode/chart/templates/helpers/_common.tpl +++ b/nitronode/chart/templates/helpers/_common.tpl @@ -88,7 +88,7 @@ Returns common environment variables - name: NITRONODE_DATABASE_USERNAME value: {{ .user }} - name: NITRONODE_DATABASE_SSLMODE - value: {{ .sslmode | default "disable" | quote }} + value: {{ .sslmode | default "require" | quote }} {{- end }} {{- range $key, $value := .Values.config.extraEnvs }} - name: {{ $key | upper }} diff --git a/nitronode/chart/values.yaml b/nitronode/chart/values.yaml index 602f5e172..c8e85e5bd 100644 --- a/nitronode/chart/values.yaml +++ b/nitronode/chart/values.yaml @@ -21,8 +21,13 @@ config: user: changeme # -- Database password password: changeme - # -- Database SSL mode (disable, require, verify-ca, verify-full) - sslmode: disable + # -- Database SSL mode (disable, require, verify-ca, verify-full). + # Defaults to `require` so any deployment that exposes Postgres over an + # untrusted network gets TLS without extra configuration. Override to + # `disable` for setups where the database is only reachable on a private + # network (e.g. a cluster-internal pgbouncer / VPC-only Cloud SQL) and TLS + # is not required; use `verify-ca` / `verify-full` for strict cert checking. + sslmode: require # -- Name of the secret containing GCP SA Credentials (Optional) gcpSaSecret: "" # -- Additional environment variables as key-value pairs