diff --git a/.github/renovate.json b/.github/renovate.json new file mode 100644 index 00000000..ce38ba96 --- /dev/null +++ b/.github/renovate.json @@ -0,0 +1,38 @@ +{ + "baseBranches": ["main"], + "username": "renovate-release", + "gitAuthor": "Renovate Bot ", + "branchPrefix": "renovateaction/", + "onboarding": false, + "extends": ["config:base", ":rebaseStalePrs"], + "ignorePresets": [":prHourlyLimit2"], + "enabledManagers": ["dockerfile", "gomod", "github-actions","regex" ], + "includeForks": true, + "repositories": ["juanfont/headscale"], + "platform": "github", + "packageRules": [ + { + "matchDatasources": ["go"], + "groupName": "Go modules", + "groupSlug": "gomod", + "separateMajorMinor": false + }, + { + "matchDatasources": ["docker"], + "groupName": "Dockerfiles", + "groupSlug": "dockerfiles" + } + ], + "regexManagers": [ + { + "fileMatch": [ + ".github/workflows/.*.yml$" + ], + "matchStrings": [ + "\\s*go-version:\\s*\"?(?.*?)\"?\\n" + ], + "datasourceTemplate": "golang-version", + "depNameTemplate": "actions/go-version" + } + ] +} diff --git a/.github/workflows/renovatebot.yml b/.github/workflows/renovatebot.yml new file mode 100644 index 00000000..53b976ca --- /dev/null +++ b/.github/workflows/renovatebot.yml @@ -0,0 +1,27 @@ +--- +name: Renovate +on: + schedule: + - cron: "* * 5,20 * *" # Every 5th and 20th of the month + workflow_dispatch: +jobs: + renovate: + runs-on: ubuntu-latest + steps: + - name: Get token + id: get_token + uses: machine-learning-apps/actions-app-token@master + with: + APP_PEM: ${{ secrets.RENOVATEBOT_SECRET }} + APP_ID: ${{ secrets.RENOVATEBOT_APP_ID }} + + - name: Checkout + uses: actions/checkout@v2.0.0 + + - name: Self-hosted Renovate + uses: renovatebot/github-action@v31.81.3 + with: + configurationFile: .github/renovate.json + token: "x-access-token:${{ steps.get_token.outputs.app_token }}" + # env: + # LOG_LEVEL: "debug" diff --git a/CHANGELOG.md b/CHANGELOG.md index 06a0ec62..531137fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,10 @@ This is a part of aligning `headscale`'s behaviour with Tailscale's upstream beh - Tags should now work correctly and adding a host to Headscale should now reload the rules. - The documentation have a [fictional example](docs/acls.md) that should cover some use cases of the ACLs features +**Features**: + +- Add support for configurable mTLS [docs](docs/tls.md#configuring-mutual-tls-authentication-mtls) [#297](https://github.com/juanfont/headscale/pull/297) + **Changes**: - Remove dependency on CGO (switch from CGO SQLite to pure Go) [#346](https://github.com/juanfont/headscale/pull/346) diff --git a/README.md b/README.md index e7ba6649..d23a216a 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,7 @@ If you would like to sponsor features, bugs or prioritisation, reach out to one | ------- | ----------------------------------------------------------------------------------------------------------------- | | Linux | Yes | | OpenBSD | Yes | +| FreeBSD | Yes | | macOS | Yes (see `/apple` on your headscale for more information) | | Windows | Yes [docs](./docs/windows-client.md) | | Android | [You need to compile the client yourself](https://github.com/juanfont/headscale/issues/58#issuecomment-885255270) | @@ -150,6 +151,13 @@ make build ohdearaugustin + + + Adrien +
+ Adrien Raffin-Caboisse +
+ Alessandro @@ -157,6 +165,8 @@ make build Alessandro (Ale) Segala + + unreality/ @@ -164,8 +174,6 @@ make build unreality - - Eugen @@ -201,6 +209,8 @@ make build Michael G. + + Paul @@ -208,8 +218,6 @@ make build Paul Tötterman - - Casey @@ -245,6 +253,8 @@ make build thomas + + Abraham @@ -252,15 +262,6 @@ make build Abraham Ingersoll - - - - - Adrien -
- Adrien Raffin-Caboisse -
- Artem @@ -305,6 +306,13 @@ make build JJGadgets + + + Jamie +
+ Jamie Greeff +
+ Jim @@ -333,6 +341,8 @@ make build Ryan Fowler + + Shaanan @@ -340,8 +350,6 @@ make build Shaanan Cohney - - Tanner/ @@ -377,6 +385,8 @@ make build Tjerk Woudsma + + Zakhar @@ -384,8 +394,6 @@ make build Zakhar Bessarab - - ZiYuan/ @@ -421,6 +429,15 @@ make build lion24 + + + + + pernila/ +
+ pernila +
+ Wakeful-Cloud/ @@ -428,8 +445,6 @@ make build Wakeful-Cloud - - zy/ diff --git a/app.go b/app.go index ac350ecf..26ec9568 100644 --- a/app.go +++ b/app.go @@ -62,6 +62,10 @@ const ( errUnsupportedLetsEncryptChallengeType = Error( "unknown value for Lets Encrypt challenge type", ) + + DisabledClientAuth = "disabled" + RelaxedClientAuth = "relaxed" + EnforcedClientAuth = "enforced" ) // Config contains the initial Headscale configuration. @@ -90,8 +94,9 @@ type Config struct { TLSLetsEncryptCacheDir string TLSLetsEncryptChallengeType string - TLSCertPath string - TLSKeyPath string + TLSCertPath string + TLSKeyPath string + TLSClientAuthMode tls.ClientAuthType ACMEURL string ACMEEmail string @@ -150,6 +155,27 @@ type Headscale struct { requestedExpiryCache *cache.Cache } +// Look up the TLS constant relative to user-supplied TLS client +// authentication mode. If an unknown mode is supplied, the default +// value, tls.RequireAnyClientCert, is returned. The returned boolean +// indicates if the supplied mode was valid. +func LookupTLSClientAuthMode(mode string) (tls.ClientAuthType, bool) { + switch mode { + case DisabledClientAuth: + // Client cert is _not_ required. + return tls.NoClientCert, true + case RelaxedClientAuth: + // Client cert required, but _not verified_. + return tls.RequireAnyClientCert, true + case EnforcedClientAuth: + // Client cert is _required and verified_. + return tls.RequireAndVerifyClientCert, true + default: + // Return the default when an unknown value is supplied. + return tls.RequireAnyClientCert, false + } +} + // NewHeadscale returns the Headscale app. func NewHeadscale(cfg Config) (*Headscale, error) { privKey, err := readOrCreatePrivateKey(cfg.PrivateKeyPath) @@ -676,12 +702,18 @@ func (h *Headscale) getTLSSettings() (*tls.Config, error) { if !strings.HasPrefix(h.cfg.ServerURL, "https://") { log.Warn().Msg("Listening with TLS but ServerURL does not start with https://") } + + log.Info().Msg(fmt.Sprintf( + "Client authentication (mTLS) is \"%s\". See the docs to learn about configuring this setting.", + h.cfg.TLSClientAuthMode)) + tlsConfig := &tls.Config{ - ClientAuth: tls.RequireAnyClientCert, + ClientAuth: h.cfg.TLSClientAuthMode, NextProtos: []string{"http/1.1"}, Certificates: make([]tls.Certificate, 1), MinVersion: tls.VersionTLS12, } + tlsConfig.Certificates[0], err = tls.LoadX509KeyPair(h.cfg.TLSCertPath, h.cfg.TLSKeyPath) return tlsConfig, err diff --git a/app_test.go b/app_test.go index 02fdce8b..53c703a6 100644 --- a/app_test.go +++ b/app_test.go @@ -65,3 +65,20 @@ func (s *Suite) ResetDB(c *check.C) { } app.db = db } + +// Enusre an error is returned when an invalid auth mode +// is supplied. +func (s *Suite) TestInvalidClientAuthMode(c *check.C) { + _, isValid := LookupTLSClientAuthMode("invalid") + c.Assert(isValid, check.Equals, false) +} + +// Ensure that all client auth modes return a nil error. +func (s *Suite) TestAuthModes(c *check.C) { + modes := []string{"disabled", "relaxed", "enforced"} + + for _, v := range modes { + _, isValid := LookupTLSClientAuthMode(v) + c.Assert(isValid, check.Equals, true) + } +} diff --git a/cmd/headscale/cli/utils.go b/cmd/headscale/cli/utils.go index 85dcae71..e3dce6b7 100644 --- a/cmd/headscale/cli/utils.go +++ b/cmd/headscale/cli/utils.go @@ -50,6 +50,7 @@ func LoadConfig(path string) error { viper.SetDefault("tls_letsencrypt_cache_dir", "/var/www/.cache") viper.SetDefault("tls_letsencrypt_challenge_type", "HTTP-01") + viper.SetDefault("tls_client_auth_mode", "relaxed") viper.SetDefault("log_level", "info") @@ -92,6 +93,20 @@ func LoadConfig(path string) error { !strings.HasPrefix(viper.GetString("server_url"), "https://") { errorText += "Fatal config error: server_url must start with https:// or http://\n" } + + _, authModeValid := headscale.LookupTLSClientAuthMode( + viper.GetString("tls_client_auth_mode"), + ) + + if !authModeValid { + errorText += fmt.Sprintf( + "Invalid tls_client_auth_mode supplied: %s. Accepted values: %s, %s, %s.", + viper.GetString("tls_client_auth_mode"), + headscale.DisabledClientAuth, + headscale.RelaxedClientAuth, + headscale.EnforcedClientAuth) + } + if errorText != "" { //nolint return errors.New(strings.TrimSuffix(errorText, "\n")) @@ -281,6 +296,10 @@ func getHeadscaleConfig() headscale.Config { Msgf("'ip_prefixes' not configured, falling back to default: %v", prefixes) } + tlsClientAuthMode, _ := headscale.LookupTLSClientAuthMode( + viper.GetString("tls_client_auth_mode"), + ) + return headscale.Config{ ServerURL: viper.GetString("server_url"), Addr: viper.GetString("listen_addr"), @@ -312,8 +331,9 @@ func getHeadscaleConfig() headscale.Config { ), TLSLetsEncryptChallengeType: viper.GetString("tls_letsencrypt_challenge_type"), - TLSCertPath: absPath(viper.GetString("tls_cert_path")), - TLSKeyPath: absPath(viper.GetString("tls_key_path")), + TLSCertPath: absPath(viper.GetString("tls_cert_path")), + TLSKeyPath: absPath(viper.GetString("tls_key_path")), + TLSClientAuthMode: tlsClientAuthMode, DNSConfig: dnsConfig, diff --git a/config-example.yaml b/config-example.yaml index ba0c653f..fe0647e0 100644 --- a/config-example.yaml +++ b/config-example.yaml @@ -105,6 +105,13 @@ acme_email: "" # Domain name to request a TLS certificate for: tls_letsencrypt_hostname: "" +# Client (Tailscale/Browser) authentication mode (mTLS) +# Acceptable values: +# - disabled: client authentication disabled +# - relaxed: client certificate is required but not verified +# - enforced: client certificate is required and verified +tls_client_auth_mode: relaxed + # Path to store certificates and metadata needed by # letsencrypt tls_letsencrypt_cache_dir: /var/lib/headscale/cache diff --git a/db.go b/db.go index 7aadd20a..feee7474 100644 --- a/db.go +++ b/db.go @@ -2,6 +2,7 @@ package headscale import ( "errors" + "time" "github.com/glebarez/sqlite" "gorm.io/driver/postgres" @@ -81,10 +82,24 @@ func (h *Headscale) openDB() (*gorm.DB, error) { switch h.dbType { case Sqlite: - db, err = gorm.Open(sqlite.Open(h.dbString), &gorm.Config{ - DisableForeignKeyConstraintWhenMigrating: true, - Logger: log, - }) + db, err = gorm.Open( + sqlite.Open(h.dbString+"?_synchronous=1&_journal_mode=WAL"), + &gorm.Config{ + DisableForeignKeyConstraintWhenMigrating: true, + Logger: log, + }, + ) + + db.Exec("PRAGMA foreign_keys=ON") + + // The pure Go SQLite library does not handle locking in + // the same way as the C based one and we cant use the gorm + // connection pool as of 2022/02/23. + sqlDB, _ := db.DB() + sqlDB.SetMaxIdleConns(1) + sqlDB.SetMaxOpenConns(1) + sqlDB.SetConnMaxIdleTime(time.Hour) + case Postgres: db, err = gorm.Open(postgres.Open(h.dbString), &gorm.Config{ DisableForeignKeyConstraintWhenMigrating: true, diff --git a/docs/tls.md b/docs/tls.md index 557cdf01..c319359a 100644 --- a/docs/tls.md +++ b/docs/tls.md @@ -29,3 +29,17 @@ headscale can also be configured to expose its web service via TLS. To configure tls_cert_path: "" tls_key_path: "" ``` + +### Configuring Mutual TLS Authentication (mTLS) + +mTLS is a method by which an HTTPS server authenticates clients, e.g. Tailscale, using TLS certificates. This can be configured by applying one of the following values to the `tls_client_auth_mode` setting in the configuration file. + +| Value | Behavior | +| ------------------- | ---------------------------------------------------------- | +| `disabled` | Disable mTLS. | +| `relaxed` (default) | A client certificate is required, but it is not verified. | +| `enforced` | Requires clients to supply a certificate that is verified. | + +```yaml +tls_client_auth_mode: "" +```