feat: auto tls
Some checks failed
continuous-integration/drone/push Build is failing

This commit introduces the automatic retrieval of TLS certificates from Let's Encrypt. If the feature is enabled, Vikunja will automagically request a certificate from Let's Encrypt and configure it to server content via TLS.
This commit is contained in:
kolaente 2024-09-29 18:20:30 +02:00
parent 6a94c39ea8
commit daa7ad053c
Signed by: konrad
GPG Key ID: F40E70337AB24C9B
3 changed files with 93 additions and 2 deletions

@ -803,6 +803,26 @@
"comment": "The proxy password to use when authenticating against the proxy."
}
]
},
{
"key": "autotls",
"children": [
{
"key": "enabled",
"default_value": "false",
"comment": "If set to true, Vikunja will automatically request a TLS certificate from Let's Encrypt and use it to serve Vikunja over TLS. By enabling this option, you agree to Let's Encrypt's TOS.\nYou must configure a `service.publicurl` with a valid TLD where Vikunja is reachable to make this work. Furthermore, it is reccomened to set `service.interface` to `:443` if you're using this."
},
{
"key": "email",
"default_value": "",
"comment": "A valid email address which will be used to register certificates with Let's Encrypt. You must provide this value in order to use autotls."
},
{
"key": "renewbefore",
"default_value": "30d",
"comment": "A duration when certificates should be renewed before they expire. Valid time units are `ns`, `us` (or `µs`), `ms`, `s`, `m`, `h`."
}
]
}
]
}

@ -19,8 +19,11 @@ package cmd
import (
"context"
"net"
"net/url"
"os"
"os/signal"
"path/filepath"
"strings"
"time"
"code.vikunja.io/api/pkg/config"
@ -33,6 +36,7 @@ import (
"github.com/labstack/echo/v4"
"github.com/spf13/cobra"
"golang.org/x/crypto/acme/autocert"
)
func init() {
@ -64,6 +68,52 @@ func setupUnixSocket(e *echo.Echo) error {
return nil
}
func setupAutoTLS(e *echo.Echo) {
if config.ServiceUnixSocket.GetString() != "" {
log.Warning("Auto tls is enabled but listening on a unix socket is enabled as well. The latter will be ignored.")
}
if config.ServicePublicURL.GetString() == "" {
log.Fatal("You must configure a publicurl to use autotls.")
}
parsed, err := url.Parse(config.ServicePublicURL.GetString())
if err != nil {
log.Fatalf("Could not parse hostname from publicurl: %s", err)
}
domain := parsed.Hostname()
if domain == "" {
log.Fatalf("The hostname cannot be empty. Please make sure the configured publicurl contains a hostname.")
}
if !strings.Contains(domain, ".") {
log.Fatalf("The hostname must be a valid TLD. Please make sure the configured publicurl contains a valid TLD.")
}
renew, err := time.ParseDuration(config.AutoTLSRenewBefore.GetString())
if err != nil {
log.Fatalf("autotls.renewbefore must be a valid duration: %s", err)
}
if config.AutoTLSEmail.GetString() == "" {
log.Fatalf("You must provide an email address to use autotls.")
}
e.AutoTLSManager = autocert.Manager{
Prompt: autocert.AcceptTOS,
Cache: autocert.DirCache(filepath.Join(
config.FilesBasePath.GetString(),
".certs",
)),
HostPolicy: autocert.HostWhitelist(domain),
RenewBefore: renew,
Email: config.AutoTLSEmail.GetString(),
}
if config.ServiceInterface.GetString() != ":443" {
log.Warningf("Vikunja's interface is set to %s, with tls it is recommended to set this to :443", config.ServiceInterface.GetString())
}
err = e.StartAutoTLS(config.ServiceInterface.GetString())
if err != nil {
e.Logger.Info("shutting down...")
}
}
var webCmd = &cobra.Command{
Use: "web",
Short: "Starts the rest api web server",
@ -80,18 +130,26 @@ var webCmd = &cobra.Command{
routes.RegisterRoutes(e)
// Start server
go func() {
if config.AutoTLSEnabled.GetBool() {
setupAutoTLS(e)
return
}
// Listen unix socket if needed (ServiceInterface will be ignored)
if config.ServiceUnixSocket.GetString() != "" {
if err := setupUnixSocket(e); err != nil {
e.Logger.Fatal(err)
}
return
}
if err := e.Start(config.ServiceInterface.GetString()); err != nil {
err := e.Start(config.ServiceInterface.GetString())
if err != nil {
e.Logger.Info("shutting down...")
}
}()
// Wait for interrupt signal to gracefully shutdown the server with
// Wait for interrupt signal to gracefully shut down the server with
// a timeout of 10 seconds.
quit := make(chan os.Signal, 1)
signal.Notify(quit, os.Interrupt)

@ -183,6 +183,10 @@ const (
WebhooksTimeoutSeconds Key = `webhooks.timeoutseconds`
WebhooksProxyURL Key = `webhooks.proxyurl`
WebhooksProxyPassword Key = `webhooks.proxypassword`
AutoTLSEnabled Key = `autotls.enabled`
AutoTLSEmail Key = `autotls.email`
AutoTLSRenewBefore Key = `autotls.renewbefore`
)
// GetString returns a string config value
@ -407,6 +411,8 @@ func InitDefaultConfig() {
// Webhook
WebhooksEnabled.setDefault(true)
WebhooksTimeoutSeconds.setDefault(30)
// AutoTLS
AutoTLSRenewBefore.setDefault("720h") // 30days in hours
}
// InitConfig initializes the config, sets defaults etc.
@ -481,6 +487,13 @@ func InitConfig() {
log.Warning("service.enablemetrics is deprecated and will be removed in a future release. Please use metrics.enable.")
MetricsEnabled.Set(true)
}
if !strings.HasPrefix(FilesBasePath.GetString(), "/") {
FilesBasePath.Set(filepath.Join(
ServiceRootpath.GetString(),
FilesBasePath.GetString(),
))
}
}
func random(length int) (string, error) {