mirror of
https://github.com/juanfont/headscale.git
synced 2024-11-30 02:43:05 +00:00
Merge pull request #9 from cure/add-tls-autocert
Add support for TLS to the web service.
This commit is contained in:
commit
d30b682a6e
5 changed files with 150 additions and 6 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
|
||||||
|
|
||||||
|
|
62
app.go
62
app.go
|
@ -1,11 +1,16 @@
|
||||||
package headscale
|
package headscale
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
"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"
|
||||||
)
|
)
|
||||||
|
@ -22,6 +27,13 @@ type Config struct {
|
||||||
DBname string
|
DBname string
|
||||||
DBuser string
|
DBuser string
|
||||||
DBpass string
|
DBpass string
|
||||||
|
|
||||||
|
TLSLetsEncryptHostname string
|
||||||
|
TLSLetsEncryptCacheDir string
|
||||||
|
TLSLetsEncryptChallengeType string
|
||||||
|
|
||||||
|
TLSCertPath string
|
||||||
|
TLSKeyPath string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Headscale represents the base app of the service
|
// Headscale represents the base app of the service
|
||||||
|
@ -61,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()
|
||||||
|
@ -68,6 +86,48 @@ func (h *Headscale) Serve() error {
|
||||||
r.GET("/register", h.RegisterWebAPI)
|
r.GET("/register", h.RegisterWebAPI)
|
||||||
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)
|
||||||
err := r.Run(h.cfg.Addr)
|
var err error
|
||||||
|
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://") {
|
||||||
|
fmt.Println("WARNING: listening without TLS but ServerURL does not start with http://")
|
||||||
|
}
|
||||||
|
err = r.Run(h.cfg.Addr)
|
||||||
|
} else {
|
||||||
|
if !strings.HasPrefix(h.cfg.ServerURL, "https://") {
|
||||||
|
fmt.Println("WARNING: listening with TLS but ServerURL does not start with https://")
|
||||||
|
}
|
||||||
|
err = r.RunTLS(h.cfg.Addr, h.cfg.TLSCertPath, h.cfg.TLSKeyPath)
|
||||||
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
@ -49,7 +49,10 @@ var serveCmd = &cobra.Command{
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Error initializing: %s", err)
|
log.Fatalf("Error initializing: %s", err)
|
||||||
}
|
}
|
||||||
h.Serve()
|
err = h.Serve()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Error initializing: %s", err)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -252,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)
|
||||||
|
@ -285,7 +308,7 @@ func main() {
|
||||||
func absPath(path string) string {
|
func absPath(path string) string {
|
||||||
// If a relative path is provided, prefix it with the the directory where
|
// If a relative path is provided, prefix it with the the directory where
|
||||||
// the config file was found.
|
// the config file was found.
|
||||||
if !strings.HasPrefix(path, "/") {
|
if (path != "") && !strings.HasPrefix(path, "/") {
|
||||||
dir, _ := filepath.Split(viper.ConfigFileUsed())
|
dir, _ := filepath.Split(viper.ConfigFileUsed())
|
||||||
if dir != "" {
|
if dir != "" {
|
||||||
path = dir + "/" + path
|
path = dir + "/" + path
|
||||||
|
@ -311,7 +334,15 @@ func getHeadscaleApp() (*headscale.Headscale, error) {
|
||||||
DBname: viper.GetString("db_name"),
|
DBname: viper.GetString("db_name"),
|
||||||
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")),
|
||||||
|
TLSKeyPath: absPath(viper.GetString("tls_key_path")),
|
||||||
}
|
}
|
||||||
|
|
||||||
h, err := headscale.NewHeadscale(cfg)
|
h, err := headscale.NewHeadscale(cfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
|
@ -7,5 +7,10 @@
|
||||||
"db_port": 5432,
|
"db_port": 5432,
|
||||||
"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_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