From 677490bc4e8d61627bcab32ed801c10a27139f29 Mon Sep 17 00:00:00 2001 From: Tobi Smethurst <31960611+tsmethurst@users.noreply.github.com> Date: Mon, 19 Jul 2021 18:03:07 +0200 Subject: [PATCH] Db tls (#102) * go mod tidy * complete example config * add tls support for db connection * add certpool to tlsConfig * add some lil docker scripts --- cmd/gotosocial/main.go | 12 +++ dockerbuild.sh | 3 + dockerpush.sh | 3 + example/config.yaml | 161 ++++++++++++++++++++++++++++++++++++- go.mod | 1 - go.sum | 4 - internal/config/config.go | 64 +++++++++------ internal/config/db.go | 33 ++++++-- internal/config/default.go | 14 ++-- internal/db/pg/pg.go | 52 ++++++++++++ 10 files changed, 302 insertions(+), 45 deletions(-) create mode 100755 dockerbuild.sh create mode 100755 dockerpush.sh diff --git a/cmd/gotosocial/main.go b/cmd/gotosocial/main.go index 9729f770..fde83e62 100644 --- a/cmd/gotosocial/main.go +++ b/cmd/gotosocial/main.go @@ -117,6 +117,18 @@ func main() { Value: defaults.DbDatabase, EnvVars: []string{envNames.DbDatabase}, }, + &cli.StringFlag{ + Name: flagNames.DbTLSMode, + Usage: "Database tls mode", + Value: defaults.DBTlsMode, + EnvVars: []string{envNames.DbTLSMode}, + }, + &cli.StringFlag{ + Name: flagNames.DbTLSCACert, + Usage: "Path to CA cert for db tls connection", + Value: defaults.DBTlsCACert, + EnvVars: []string{envNames.DbTLSCACert}, + }, // TEMPLATE FLAGS &cli.StringFlag{ diff --git a/dockerbuild.sh b/dockerbuild.sh new file mode 100755 index 00000000..87893c65 --- /dev/null +++ b/dockerbuild.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +docker build -t "superseriousbusiness/gotosocial:$(cat version)" . diff --git a/dockerpush.sh b/dockerpush.sh new file mode 100755 index 00000000..8377f8e4 --- /dev/null +++ b/dockerpush.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +docker push "superseriousbusiness/gotosocial:$(cat version)" diff --git a/example/config.yaml b/example/config.yaml index 81e4727e..b2632281 100644 --- a/example/config.yaml +++ b/example/config.yaml @@ -18,7 +18,7 @@ ##### GENERAL CONFIG ###### ########################### # String. Log level to use throughout the application. Must be lower-case. -# Options: ["debug","info","warn","error","fatal"] +# Options: ["trace","debug","info","warn","error","fatal"] # Default: "info" logLevel: "info" @@ -66,14 +66,29 @@ db: # REQUIRED # String. Password to use for the database connection # Examples: ["password123","verysafepassword","postgres"] - # Default: "" - password: "" + # Default: "postgres" + password: "postgres" # String. Name of the database to use within the provided database type. # Examples: ["mydb","postgres","gotosocial"] # Default: "postgres" database: "postgres" + # String. Disable, enable, or require SSL/TLS connection to the database. + # If "disable" then no TLS connection will be attempted. + # If "enable" then TLS will be tried, but the database certificate won't be checked (for self-signed certs). + # If "require" then TLS will be required to make a connection, and a valid certificate must be presented. + # Options: ["disable", "enable", "require"] + # Default: "disable" + tlsMode: "disable" + + # String. Path to a CA certificate on the host machine for db certificate validation. + # If this is left empty, just the host certificates will be used. + # If filled in, the certificate will be loaded and added to host certificates. + # Examples: ["/path/to/some/cert.crt"] + # Default: "" + tlsCACert: "" + ############################### ##### WEB TEMPLATE CONFIG ##### ############################### @@ -84,6 +99,11 @@ template: # Default: "./web/template/" baseDir: "./web/template/" + # String. Directory from which gotosocial will attempt to serve static web assets (images, scripts). + # Examples: ["/some/absolute/path/", "./relative/path/", "../../some/weird/path/"] + # Default: "./web/assets/" + assetBaseDir: "./web/assets/" + ########################### ##### ACCOUNTS CONFIG ##### ########################### @@ -93,7 +113,142 @@ accounts: # Options: [true, false] # Default: true openRegistration: true + # Bool. Do sign up requests require approval from an admin/moderator before an account can sign in/use the server? # Options: [true, false] # Default: true requireApproval: true + + # Bool. Are sign up requests required to submit a reason for the request (eg., an explanation of why they want to join the instance)? + # Options: [true, false] + # Default: true + reasonRequired: true + +######################## +##### MEDIA CONFIG ##### +######################## +# Config pertaining to user media uploads (videos, image, image descriptions). +media: + # Int. Maximum allowed image upload size in bytes. + # Examples: [2097152, 10485760] + # Default: 2097152 -- aka 2MB + maxImageSize: 2097152 + + # Int. Maximum allowed video upload size in bytes. + # Examples: [2097152, 10485760] + # Default: 10485760 -- aka 10MB + maxVideoSize: 10485760 + + # Int. Minimum amount of characters required as an image or video description. + # Examples: [500, 1000, 1500] + # Default: 0 (not required) + minDescriptionChars: 0 + + # Int. Maximum amount of characters permitted in an image or video description. + # Examples: [500, 1000, 1500] + # Default: 500 + maxDescriptionChars: 500 + +########################## +##### STORAGE CONFIG ##### +########################## +# Config pertaining to storage of user-created uploads (videos, images, etc). +storage: + # String. Type of storage backend to use. + # Examples: ["local", "s3"] + # Default: "local" (storage on local disk) + # NOTE: s3 storage is not yet supported! + backend: "local" + + # String. Directory to use as a base path for storing files. + # Make sure whatever user/group gotosocial is running as has permission to access + # this directly, and create new subdirectories and files with in. + # Examples: ["/home/gotosocial/storage", "/opt/gotosocial/datastorage"] + # Default: "/gotosocial/storage" + basePath: "/gotosocial/storage" + + # String. Protocol to use for serving stored files. + # It's very unlikely that you'll need to change this ever, but there might be edge cases. + # Examples: ["http", "https"] + serveProtocol: "https" + + # String. Host for serving stored files. + # If you're using local storage, this should be THE SAME as the value you've set for Host, above. + # It should only be a different value if you're serving stored files from a host + # other than the one your instance is running on. + # Examples: ["localhost", "example.org"] + # Default: "localhost" -- you should absolutely change this. + serveHost: "localhost" + + # String. Base path for serving stored files. This will be added to serveHost and serveProtocol + # to form the prefix url of your stored files. Eg., https://example.org/fileserver/..... + # It's unlikely that you will need to change this. + # Examples: ["/fileserver", "/media"] + # Default: "/fileserver" + serveBasePath: "/fileserver" + +########################### +##### STATUSES CONFIG ##### +########################### +# Config pertaining to the creation of statuses/posts, and permitted limits. +statuses: + # Int. Maximum amount of characters permitted for a new status. + # Note that going way higher than the default might break federation. + # Examples: [140, 500, 5000] + # Default: 5000 + maxChars: 5000 + + # Int. Maximum amount of characters allowed in the CW/subject header of a status. + # Note that going way higher than the default might break federation. + # Examples: [100, 200] + # Default: 100 + cwMaxChars: 100 + + # Int. Maximum amount of options to permit when creating a new poll. + # Note that going way higher than the default might break federation. + # Examples: [4, 6, 10] + # Default: 6 + pollMaxOptions: 6 + + # Int. Maximum amount of characters to permit per poll option when creating a new poll. + # Note that going way higher than the default might break federation. + # Examples: [50, 100, 150] + # Default: 50 + pollOptionMaxChars: 50 + + # Int. Maximum amount of media files that can be attached to a new status. + # Note that going way higher than the default might break federation. + # Examples: [4, 6, 10] + # Default: 6 + maxMediaFiles: 6 + +############################## +##### LETSENCRYPT CONFIG ##### +############################## +# Config pertaining to the automatic acquisition and use of LetsEncrypt HTTPS certificates. +letsEncrypt: + # Bool. Whether or not letsencrypt should be enabled for the server. + # If true, the server will serve on port 443 (https) and obtain letsencrypt + # certificates automatically. + # If false, the server will serve on port 8080 (http), and the rest of the settings + # here will be ignored. + # You should only change this if you want to serve GoToSocial behind a reverse proxy + # like Traefik, HAProxy, or Nginx. + # Options: [true, false] + # Default: true + enabled: true + + # String. Directory in which to store LetsEncrypt certificates. + # It is a good move to make this a sub-path within your storage directory, as it makes + # backup easier, but you might wish to move them elsewhere if they're also accessed by other services. + # In any case, make sure GoToSocial has permissions to write to / read from this directory. + # Examples: ["/home/gotosocial/storage/certs", "/acmecerts"] + # Default: "/gotosocial/storage/certs" + certDir: "/gotosocial/storage/certs" + + # String. Email address to use when registering LetsEncrypt certs. + # Most likely, this will be the email address of the instance administrator. + # LetsEncrypt will send notifications about expiring certificates etc to this address. + # Examples: ["admin@example.org"] + # Default: "" + emailAddress: "" diff --git a/go.mod b/go.mod index 31dfa9fb..47b27f35 100644 --- a/go.mod +++ b/go.mod @@ -15,7 +15,6 @@ require ( github.com/dsoprea/go-utility v0.0.0-20200717064901-2fccff4aa15e // indirect github.com/gin-contrib/cors v1.3.1 github.com/gin-contrib/sessions v0.0.3 - github.com/gin-contrib/static v0.0.1 github.com/gin-gonic/gin v1.7.2 github.com/go-errors/errors v1.4.0 // indirect github.com/go-fed/activity v1.0.1-0.20210426194615-e0de0863dcc1 diff --git a/go.sum b/go.sum index 6926a115..08ee5d58 100644 --- a/go.sum +++ b/go.sum @@ -69,10 +69,7 @@ github.com/gin-contrib/sessions v0.0.3 h1:PoBXki+44XdJdlgDqDrY5nDVe3Wk7wDV/UCOuL github.com/gin-contrib/sessions v0.0.3/go.mod h1:8C/J6cad3Il1mWYYgtw0w+hqasmpvy25mPkXdOgeB9I= github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= -github.com/gin-contrib/static v0.0.1 h1:JVxuvHPuUfkoul12N7dtQw7KRn/pSMq7Ue1Va9Swm1U= -github.com/gin-contrib/static v0.0.1/go.mod h1:CSxeF+wep05e0kCOsqWdAWbSszmc31zTIbD8TvWl7Hs= github.com/gin-gonic/gin v1.5.0/go.mod h1:Nd6IXA8m5kNZdNEHMBd93KT+mdY3+bewLgRvmCsR2Do= -github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= github.com/gin-gonic/gin v1.7.2 h1:Tg03T9yM2xa8j6I3Z3oqLaQRSmKvxPd6g/2HJ6zICFA= github.com/gin-gonic/gin v1.7.2/go.mod h1:jD2toBW3GZUr5UMcdrwQA10I7RuaFOl/SGeDjXkfUtY= github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= @@ -101,7 +98,6 @@ github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTM github.com/go-playground/universal-translator v0.16.0/go.mod h1:1AnU7NaIRDWWzGEKwgtJRd2xk99HeFyHw3yid4rvQIY= github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= -github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= github.com/go-playground/validator/v10 v10.6.1 h1:W6TRDXt4WcWp4c4nf/G+6BkGdhiIo0k417gfr+V6u4I= github.com/go-playground/validator/v10 v10.6.1/go.mod h1:xm76BBt941f7yWdGnI2DVPFFg1UK3YY04qifoXU3lOk= diff --git a/internal/config/config.go b/internal/config/config.go index 28bbc854..323b7de8 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -165,6 +165,14 @@ func (c *Config) ParseCLIFlags(f KeyedFlags, version string) error { c.DBConfig.Database = f.String(fn.DbDatabase) } + if c.DBConfig.TLSMode == DBTLSModeUnset || f.IsSet(fn.DbTLSMode) { + c.DBConfig.TLSMode = DBTLSMode(f.String(fn.DbTLSMode)) + } + + if c.DBConfig.TLSCACert == "" || f.IsSet(fn.DbTLSCACert) { + c.DBConfig.TLSCACert = f.String(fn.DbTLSCACert) + } + // template flags if c.TemplateConfig.BaseDir == "" || f.IsSet(fn.TemplateBaseDir) { c.TemplateConfig.BaseDir = f.String(fn.TemplateBaseDir) @@ -284,12 +292,14 @@ type Flags struct { Host string Protocol string - DbType string - DbAddress string - DbPort string - DbUser string - DbPassword string - DbDatabase string + DbType string + DbAddress string + DbPort string + DbUser string + DbPassword string + DbDatabase string + DbTLSMode string + DbTLSCACert string TemplateBaseDir string AssetBaseDir string @@ -329,12 +339,14 @@ type Defaults struct { Protocol string SoftwareVersion string - DbType string - DbAddress string - DbPort int - DbUser string - DbPassword string - DbDatabase string + DbType string + DbAddress string + DbPort int + DbUser string + DbPassword string + DbDatabase string + DBTlsMode string + DBTlsCACert string TemplateBaseDir string AssetBaseDir string @@ -375,12 +387,14 @@ func GetFlagNames() Flags { Host: "host", Protocol: "protocol", - DbType: "db-type", - DbAddress: "db-address", - DbPort: "db-port", - DbUser: "db-user", - DbPassword: "db-password", - DbDatabase: "db-database", + DbType: "db-type", + DbAddress: "db-address", + DbPort: "db-port", + DbUser: "db-user", + DbPassword: "db-password", + DbDatabase: "db-database", + DbTLSMode: "db-tls-mode", + DbTLSCACert: "db-tls-ca-cert", TemplateBaseDir: "template-basedir", AssetBaseDir: "asset-basedir", @@ -422,12 +436,14 @@ func GetEnvNames() Flags { Host: "GTS_HOST", Protocol: "GTS_PROTOCOL", - DbType: "GTS_DB_TYPE", - DbAddress: "GTS_DB_ADDRESS", - DbPort: "GTS_DB_PORT", - DbUser: "GTS_DB_USER", - DbPassword: "GTS_DB_PASSWORD", - DbDatabase: "GTS_DB_DATABASE", + DbType: "GTS_DB_TYPE", + DbAddress: "GTS_DB_ADDRESS", + DbPort: "GTS_DB_PORT", + DbUser: "GTS_DB_USER", + DbPassword: "GTS_DB_PASSWORD", + DbDatabase: "GTS_DB_DATABASE", + DbTLSMode: "GTS_DB_TLS_MODE", + DbTLSCACert: "GTS_DB_CA_CERT", TemplateBaseDir: "GTS_TEMPLATE_BASEDIR", AssetBaseDir: "GTS_ASSET_BASEDIR", diff --git a/internal/config/db.go b/internal/config/db.go index fbde6fe8..7ea71a8b 100644 --- a/internal/config/db.go +++ b/internal/config/db.go @@ -20,11 +20,30 @@ package config // DBConfig provides configuration options for the database connection type DBConfig struct { - Type string `yaml:"type"` - Address string `yaml:"address"` - Port int `yaml:"port"` - User string `yaml:"user"` - Password string `yaml:"password"` - Database string `yaml:"database"` - ApplicationName string `yaml:"applicationName"` + Type string `yaml:"type"` + Address string `yaml:"address"` + Port int `yaml:"port"` + User string `yaml:"user"` + Password string `yaml:"password"` + Database string `yaml:"database"` + ApplicationName string `yaml:"applicationName"` + TLSMode DBTLSMode `yaml:"tlsMode"` + TLSCACert string `yaml:"tlsCACert"` } + +// DBTLSMode describes a mode of connecting to a database with or without TLS. +type DBTLSMode string + +// DBTLSModeDisable does not attempt to make a TLS connection to the database. +var DBTLSModeDisable DBTLSMode = "disable" + +// DBTLSModeEnable attempts to make a TLS connection to the database, but doesn't fail if +// the certificate passed by the database isn't verified. +var DBTLSModeEnable DBTLSMode = "enable" + +// DBTLSModeRequire attempts to make a TLS connection to the database, and requires +// that the certificate presented by the database is valid. +var DBTLSModeRequire DBTLSMode = "require" + +// DBTLSModeUnset means that the TLS mode has not been set. +var DBTLSModeUnset DBTLSMode = "" diff --git a/internal/config/default.go b/internal/config/default.go index 40df4c57..7a030beb 100644 --- a/internal/config/default.go +++ b/internal/config/default.go @@ -120,12 +120,14 @@ func GetDefaults() Defaults { Host: "", Protocol: "https", - DbType: "postgres", - DbAddress: "localhost", - DbPort: 5432, - DbUser: "postgres", - DbPassword: "postgres", - DbDatabase: "postgres", + DbType: "postgres", + DbAddress: "localhost", + DbPort: 5432, + DbUser: "postgres", + DbPassword: "postgres", + DbDatabase: "postgres", + DBTlsMode: "disable", + DBTlsCACert: "", TemplateBaseDir: "./web/template/", AssetBaseDir: "./web/assets/", diff --git a/internal/db/pg/pg.go b/internal/db/pg/pg.go index ad75cef1..5301f041 100644 --- a/internal/db/pg/pg.go +++ b/internal/db/pg/pg.go @@ -22,10 +22,14 @@ import ( "context" "crypto/rand" "crypto/rsa" + "crypto/tls" + "crypto/x509" + "encoding/pem" "errors" "fmt" "net" "net/mail" + "os" "strings" "time" @@ -133,6 +137,53 @@ func derivePGOptions(c *config.Config) (*pg.Options, error) { return nil, errors.New("no database set") } + var tlsConfig *tls.Config + switch c.DBConfig.TLSMode { + case config.DBTLSModeDisable, config.DBTLSModeUnset: + break // nothing to do + case config.DBTLSModeEnable: + tlsConfig = &tls.Config{ + InsecureSkipVerify: true, + } + case config.DBTLSModeRequire: + tlsConfig = &tls.Config{ + InsecureSkipVerify: false, + } + } + + if tlsConfig != nil && c.DBConfig.TLSCACert != "" { + // load the system cert pool first -- we'll append the given CA cert to this + certPool, err := x509.SystemCertPool() + if err != nil { + return nil, fmt.Errorf("error fetching system CA cert pool: %s", err) + } + + // open the file itself and make sure there's something in it + caCertBytes, err := os.ReadFile(c.DBConfig.TLSCACert) + if err != nil { + return nil, fmt.Errorf("error opening CA certificate at %s: %s", c.DBConfig.TLSCACert, err) + } + if len(caCertBytes) == 0 { + return nil, fmt.Errorf("ca cert at %s was empty", c.DBConfig.TLSCACert) + } + + // make sure we have a PEM block + caPem, _ := pem.Decode(caCertBytes) + if caPem == nil { + return nil, fmt.Errorf("could not parse cert at %s into PEM", c.DBConfig.TLSCACert) + } + + // parse the PEM block into the certificate + caCert, err := x509.ParseCertificate(caPem.Bytes) + if err != nil { + return nil, fmt.Errorf("could not parse cert at %s into x509 certificate: %s", c.DBConfig.TLSCACert, err) + } + + // we're happy, add it to the existing pool and then use this pool in our tls config + certPool.AddCert(caCert) + tlsConfig.RootCAs = certPool + } + // We can rely on the pg library we're using to set // sensible defaults for everything we don't set here. options := &pg.Options{ @@ -141,6 +192,7 @@ func derivePGOptions(c *config.Config) (*pg.Options, error) { Password: c.DBConfig.Password, Database: c.DBConfig.Database, ApplicationName: c.ApplicationName, + TLSConfig: tlsConfig, } return options, nil