Add support for automatic TLS certificates via Let's Encrypt. Add a
configuration reference to the README.md file.
This commit is contained in:
parent
1b30874cf8
commit
426b4fd98a
5 changed files with 123 additions and 3 deletions
49
README.md
49
README.md
|
@ -78,6 +78,55 @@ Suggestions/PRs welcomed!
|
||||||
./headscale register YOURMACHINEKEY myfirstnamespace
|
./headscale register YOURMACHINEKEY myfirstnamespace
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Configuration reference
|
||||||
|
|
||||||
|
Headscale's configuration file is named `config.json` or `config.yaml`. Headscale will look for it in `/etc/headscale`, `~/.headscale` and finally the directory from where the Headscale binary is executed.
|
||||||
|
|
||||||
|
```
|
||||||
|
"server_url": "http://192.168.1.12:8000",
|
||||||
|
"listen_addr": "0.0.0.0:8000",
|
||||||
|
```
|
||||||
|
|
||||||
|
`server_url` is the external URL via which Headscale is reachable. `listen_addr` is the IP address and port the Headscale program should listen on.
|
||||||
|
|
||||||
|
```
|
||||||
|
"private_key_path": "private.key",
|
||||||
|
```
|
||||||
|
|
||||||
|
`private_key_path` is the path to the Wireguard private key. If the path is relative, it will be interpreted as relative to the directory the configuration file was read from.
|
||||||
|
|
||||||
|
```
|
||||||
|
"derp_map_path": "derp.yaml",
|
||||||
|
```
|
||||||
|
|
||||||
|
`derp_map_path` is the path to the [DERP](https://pkg.go.dev/tailscale.com/derp) map file. If the path is relative, it will be interpreted as relative to the directory the configuration file was read from.
|
||||||
|
|
||||||
|
```
|
||||||
|
"db_host": "localhost",
|
||||||
|
"db_port": 5432,
|
||||||
|
"db_name": "headscale",
|
||||||
|
"db_user": "foo",
|
||||||
|
"db_pass": "bar",
|
||||||
|
```
|
||||||
|
|
||||||
|
The fields starting with `db_` are used for the PostgreSQL connection information.
|
||||||
|
|
||||||
|
### Running the service via TLS (optional)
|
||||||
|
|
||||||
|
```
|
||||||
|
"tls_cert_path": ""
|
||||||
|
"tls_key_path": ""
|
||||||
|
```
|
||||||
|
|
||||||
|
Headscale can be configured to expose its web service via TLS. To configure the certificate and key file manually, set the `tls_cert_path` and `tls_cert_path` configuration parameters. If the path is relative, it will be interpreted as relative to the directory the configuration file was read from.
|
||||||
|
|
||||||
|
```
|
||||||
|
"tls_letsencrypt_hostname": "",
|
||||||
|
"tls_letsencrypt_cache_dir": ".cache",
|
||||||
|
"tls_letsencrypt_challenge_type": "HTTP-01",
|
||||||
|
```
|
||||||
|
|
||||||
|
To get a certificate automatically via [Let's Encrypt](https://letsencrypt.org/), set `tls_letsencrypt_hostname` to the desired certificate hostname. This name must resolve to the IP address(es) Headscale is reachable on (i.e., it must correspond to the `server_url` configuration parameter). The certificate and Let's Encrypt account credentials will be stored in the directory configured in `tls_letsencrypt_cache_dir`. If the path is relative, it will be interpreted as relative to the directory the configuration file was read from. The certificate will automatically be renewed as needed. The default challenge type HTTP-01 requires that Headscale listens on port 80 for the Let's Encrypt automated validation, in addition to whatever port is configured in `listen_addr`. Alternatively, `tls_letsencrypt_challenge_type` can be set to `TLS-ALPN-01`. In this configuration, Headscale must be reachable via port 443, but port 80 is not required.
|
||||||
|
|
||||||
## Disclaimer
|
## Disclaimer
|
||||||
|
|
||||||
|
|
47
app.go
47
app.go
|
@ -1,12 +1,16 @@
|
||||||
package headscale
|
package headscale
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
"golang.org/x/crypto/acme/autocert"
|
||||||
"tailscale.com/tailcfg"
|
"tailscale.com/tailcfg"
|
||||||
"tailscale.com/wgengine/wgcfg"
|
"tailscale.com/wgengine/wgcfg"
|
||||||
)
|
)
|
||||||
|
@ -24,6 +28,10 @@ type Config struct {
|
||||||
DBuser string
|
DBuser string
|
||||||
DBpass string
|
DBpass string
|
||||||
|
|
||||||
|
TLSLetsEncryptHostname string
|
||||||
|
TLSLetsEncryptCacheDir string
|
||||||
|
TLSLetsEncryptChallengeType string
|
||||||
|
|
||||||
TLSCertPath string
|
TLSCertPath string
|
||||||
TLSKeyPath string
|
TLSKeyPath string
|
||||||
}
|
}
|
||||||
|
@ -65,6 +73,12 @@ func NewHeadscale(cfg Config) (*Headscale, error) {
|
||||||
return &h, nil
|
return &h, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Redirect to our TLS url
|
||||||
|
func (h *Headscale) redirect(w http.ResponseWriter, req *http.Request) {
|
||||||
|
target := h.cfg.ServerURL + req.URL.RequestURI()
|
||||||
|
http.Redirect(w, req, target, http.StatusFound)
|
||||||
|
}
|
||||||
|
|
||||||
// Serve launches a GIN server with the Headscale API
|
// Serve launches a GIN server with the Headscale API
|
||||||
func (h *Headscale) Serve() error {
|
func (h *Headscale) Serve() error {
|
||||||
r := gin.Default()
|
r := gin.Default()
|
||||||
|
@ -73,7 +87,38 @@ func (h *Headscale) Serve() error {
|
||||||
r.POST("/machine/:id/map", h.PollNetMapHandler)
|
r.POST("/machine/:id/map", h.PollNetMapHandler)
|
||||||
r.POST("/machine/:id", h.RegistrationHandler)
|
r.POST("/machine/:id", h.RegistrationHandler)
|
||||||
var err error
|
var err error
|
||||||
if h.cfg.TLSCertPath == "" {
|
if h.cfg.TLSLetsEncryptHostname != "" {
|
||||||
|
if !strings.HasPrefix(h.cfg.ServerURL, "https://") {
|
||||||
|
fmt.Println("WARNING: listening with TLS but ServerURL does not start with https://")
|
||||||
|
}
|
||||||
|
|
||||||
|
m := autocert.Manager{
|
||||||
|
Prompt: autocert.AcceptTOS,
|
||||||
|
HostPolicy: autocert.HostWhitelist(h.cfg.TLSLetsEncryptHostname),
|
||||||
|
Cache: autocert.DirCache(h.cfg.TLSLetsEncryptCacheDir),
|
||||||
|
}
|
||||||
|
s := &http.Server{
|
||||||
|
Addr: h.cfg.Addr,
|
||||||
|
TLSConfig: m.TLSConfig(),
|
||||||
|
Handler: r,
|
||||||
|
}
|
||||||
|
if h.cfg.TLSLetsEncryptChallengeType == "TLS-ALPN-01" {
|
||||||
|
// Configuration via autocert with TLS-ALPN-01 (https://tools.ietf.org/html/rfc8737)
|
||||||
|
// The RFC requires that the validation is done on port 443; in other words, headscale
|
||||||
|
// must be configured to run on port 443.
|
||||||
|
err = s.ListenAndServeTLS("", "")
|
||||||
|
} else if h.cfg.TLSLetsEncryptChallengeType == "HTTP-01" {
|
||||||
|
// Configuration via autocert with HTTP-01. This requires listening on
|
||||||
|
// port 80 for the certificate validation in addition to the headscale
|
||||||
|
// service, which can be configured to run on any other port.
|
||||||
|
go func() {
|
||||||
|
log.Fatal(http.ListenAndServe(":http", m.HTTPHandler(http.HandlerFunc(h.redirect))))
|
||||||
|
}()
|
||||||
|
} else {
|
||||||
|
return errors.New("Unknown value for TLSLetsEncryptChallengeType")
|
||||||
|
}
|
||||||
|
err = s.ListenAndServeTLS("", "")
|
||||||
|
} else if h.cfg.TLSCertPath == "" {
|
||||||
if !strings.HasPrefix(h.cfg.ServerURL, "http://") {
|
if !strings.HasPrefix(h.cfg.ServerURL, "http://") {
|
||||||
fmt.Println("WARNING: listening without TLS but ServerURL does not start with http://")
|
fmt.Println("WARNING: listening without TLS but ServerURL does not start with http://")
|
||||||
}
|
}
|
||||||
|
|
|
@ -255,11 +255,31 @@ func main() {
|
||||||
viper.AddConfigPath("$HOME/.headscale")
|
viper.AddConfigPath("$HOME/.headscale")
|
||||||
viper.AddConfigPath(".")
|
viper.AddConfigPath(".")
|
||||||
viper.AutomaticEnv()
|
viper.AutomaticEnv()
|
||||||
|
|
||||||
|
viper.SetDefault("tls_letsencrypt_cache_dir", "/var/www/.cache")
|
||||||
|
viper.SetDefault("tls_letsencrypt_challenge_type", "HTTP-01")
|
||||||
|
|
||||||
err := viper.ReadInConfig()
|
err := viper.ReadInConfig()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Fatal error config file: %s \n", err)
|
log.Fatalf("Fatal error config file: %s \n", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (viper.GetString("tls_letsencrypt_hostname") != "") && ((viper.GetString("tls_cert_path") != "") || (viper.GetString("tls_key_path") != "")) {
|
||||||
|
log.Fatalf("Fatal config error: set either tls_letsencrypt_hostname or tls_cert_path/tls_key_path, not both")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (viper.GetString("tls_letsencrypt_hostname") != "") && (viper.GetString("tls_letsencrypt_challenge_type") == "TLS-ALPN-01") && (!strings.HasSuffix(viper.GetString("listen_addr"), ":443")) {
|
||||||
|
log.Fatalf("Fatal config error: when using tls_letsencrypt_hostname with TLS-ALPN-01 as challenge type, listen_addr must end in :443")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (viper.GetString("tls_letsencrypt_challenge_type") != "HTTP-01") && (viper.GetString("tls_letsencrypt_challenge_type") != "TLS-ALPN-01") {
|
||||||
|
log.Fatalf("Fatal config error: the only supported values for tls_letsencrypt_challenge_type are HTTP-01 and TLS-ALPN-01")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.HasPrefix(viper.GetString("server_url"), "http://") && !strings.HasPrefix(viper.GetString("server_url"), "https://") {
|
||||||
|
log.Fatalf("Fatal config error: server_url must start with https:// or http://")
|
||||||
|
}
|
||||||
|
|
||||||
headscaleCmd.AddCommand(versionCmd)
|
headscaleCmd.AddCommand(versionCmd)
|
||||||
headscaleCmd.AddCommand(serveCmd)
|
headscaleCmd.AddCommand(serveCmd)
|
||||||
headscaleCmd.AddCommand(registerCmd)
|
headscaleCmd.AddCommand(registerCmd)
|
||||||
|
@ -315,6 +335,10 @@ func getHeadscaleApp() (*headscale.Headscale, error) {
|
||||||
DBuser: viper.GetString("db_user"),
|
DBuser: viper.GetString("db_user"),
|
||||||
DBpass: viper.GetString("db_pass"),
|
DBpass: viper.GetString("db_pass"),
|
||||||
|
|
||||||
|
TLSLetsEncryptHostname: viper.GetString("tls_letsencrypt_hostname"),
|
||||||
|
TLSLetsEncryptCacheDir: absPath(viper.GetString("tls_letsencrypt_cache_dir")),
|
||||||
|
TLSLetsEncryptChallengeType: viper.GetString("tls_letsencrypt_challenge_type"),
|
||||||
|
|
||||||
TLSCertPath: absPath(viper.GetString("tls_cert_path")),
|
TLSCertPath: absPath(viper.GetString("tls_cert_path")),
|
||||||
TLSKeyPath: absPath(viper.GetString("tls_key_path")),
|
TLSKeyPath: absPath(viper.GetString("tls_key_path")),
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,9 @@
|
||||||
"db_name": "headscale",
|
"db_name": "headscale",
|
||||||
"db_user": "foo",
|
"db_user": "foo",
|
||||||
"db_pass": "bar",
|
"db_pass": "bar",
|
||||||
|
"tls_letsencrypt_hostname": "",
|
||||||
|
"tls_letsencrypt_cache_dir": ".cache",
|
||||||
|
"tls_letsencrypt_challenge_type": "HTTP-01",
|
||||||
"tls_cert_path": "",
|
"tls_cert_path": "",
|
||||||
"tls_key_path": ""
|
"tls_key_path": ""
|
||||||
}
|
}
|
||||||
|
|
3
go.mod
3
go.mod
|
@ -3,9 +3,8 @@ module github.com/juanfont/headscale
|
||||||
go 1.16
|
go 1.16
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/davecgh/go-spew v1.1.1
|
|
||||||
github.com/gin-gonic/gin v1.6.3
|
github.com/gin-gonic/gin v1.6.3
|
||||||
github.com/hako/durafmt v0.0.0-20210316092057-3a2c319c1acd // indirect
|
github.com/hako/durafmt v0.0.0-20210316092057-3a2c319c1acd
|
||||||
github.com/jinzhu/gorm v1.9.16
|
github.com/jinzhu/gorm v1.9.16
|
||||||
github.com/klauspost/compress v1.11.12
|
github.com/klauspost/compress v1.11.12
|
||||||
github.com/spf13/cobra v1.1.3
|
github.com/spf13/cobra v1.1.3
|
||||||
|
|
Loading…
Reference in a new issue