diff --git a/.env b/.env index 5b2076f137d..4c8ad614fff 100644 --- a/.env +++ b/.env @@ -46,9 +46,15 @@ SHELLHUB_NETWORK=shellhub_network # Enable tunnels feature. SHELLHUB_TUNNELS=false -# The domain used to create the tunnels. If empty, the [SHELLHUB_DOMAIN] will be used. +# The domain used to create the tunnels. +# NOTICE: If empty, the [SHELLHUB_DOMAIN] will be used. SHELLHUB_TUNNELS_DOMAIN= +# The token used to generate wildcard SSL certificate using DNS method for tunnels' URL. +# Currently, only DigitalOcean is supported. +# NOTICE: Required if SHELLHUB_AUTO_SSL is defined. +SHELLHUB_TUNNELS_DNS_PROVIDER_TOKEN= + # Specifies an alternative mirror URL for downloading the GeoIP databases. This # field takes precedence over SHELLHUB_MAXMIND_LICENSE; when both are # configured, SHELLHUB_MAXMIND_MIRROR will be used as the primary source for diff --git a/docker-compose.yml b/docker-compose.yml index 45ee86e17e8..047d3716b9f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -94,6 +94,7 @@ services: - SHELLHUB_DOMAIN=${SHELLHUB_DOMAIN} - SHELLHUB_TUNNELS=${SHELLHUB_TUNNELS} - SHELLHUB_TUNNELS_DOMAIN=${SHELLHUB_TUNNELS_DOMAIN} + - SHELLHUB_TUNNELS_DNS_PROVIDER_TOKEN=${SHELLHUB_TUNNELS_DNS_PROVIDER_TOKEN} - SHELLHUB_VERSION=${SHELLHUB_VERSION} - SHELLHUB_SSH_PORT=${SHELLHUB_SSH_PORT} - SHELLHUB_PROXY=${SHELLHUB_PROXY} diff --git a/gateway/Dockerfile b/gateway/Dockerfile index cb79e7c5641..0e9b17bae92 100644 --- a/gateway/Dockerfile +++ b/gateway/Dockerfile @@ -1,9 +1,9 @@ # base stage -FROM golang:1.22.6-alpine3.19 AS base +FROM golang:1.22.6-alpine3.20 AS base ARG GOPROXY -RUN apk add --no-cache git ca-certificates libgcc certbot certbot-nginx curl +RUN apk add --no-cache git ca-certificates libgcc curl certbot certbot-nginx certbot-dns certbot-dns-digitalocean WORKDIR $GOPATH/src/github.com/shellhub-io/shellhub @@ -58,7 +58,7 @@ ENTRYPOINT ["/entrypoint.sh"] # production stage FROM alpine:3.21.0 AS production -RUN apk add libgcc curl certbot certbot-nginx +RUN apk add libgcc curl certbot certbot-nginx certbot-dns certbot-dns-digitalocean COPY --from=openresty/openresty:1.25.3.1-5-alpine-apk /usr/local/openresty /usr/local/openresty diff --git a/gateway/certbot.go b/gateway/certbot.go index cf5785b4903..055f07031a1 100644 --- a/gateway/certbot.go +++ b/gateway/certbot.go @@ -13,12 +13,24 @@ import ( "github.com/pkg/errors" ) +// DNSProvider represents a DNS provider to generate certificates. +type DNSProvider string + +// DigitalOceanDNSProvider represents the Digital Ocean DNS provider. +const DigitalOceanDNSProvider = "digitalocean" + +type tunnels struct { + domain string + token string +} + // CertBot handles the generation and renewal of SSL certificates. type CertBot struct { rootDir string domain string staging bool renewedCallback func() + tunnels *tunnels } // ensureCertificates checks if the SSL certificate exists and generates it if not. @@ -27,6 +39,13 @@ func (cb *CertBot) ensureCertificates() { if _, err := os.Stat(certPath); os.IsNotExist(err) { cb.generateCertificate() } + + if cb.tunnels != nil { + certPath := fmt.Sprintf("%s/live/*.%s/fullchain.pem", cb.rootDir, cb.tunnels.domain) + if _, err := os.Stat(certPath); os.IsNotExist(err) { + cb.generateCertificateFromDNS(DigitalOceanDNSProvider) + } + } } // generateCertificate generates a new SSL certificate using Certbot. @@ -65,6 +84,39 @@ func (cb *CertBot) generateCertificate() { cb.stopACMEServer(acmeServer) } +func (cb *CertBot) generateCertificateFromDNS(provider DNSProvider) { + fmt.Println("Generating SSL certificate with DNS") + + token := fmt.Sprintf("dns_%s_token = %s", provider, cb.tunnels.token) + + tmp := os.TempDir() + file, _ := os.CreateTemp(tmp, string(provider)) + file.Write([]byte(token)) + + cmd := exec.Command( + "certbot", + "certonly", + "--non-interactive", + "--agree-tos", + "--register-unsafely-without-email", + "--cert-name", + fmt.Sprintf("*.%s", cb.tunnels.domain), + fmt.Sprintf("--dns-%s", provider), + fmt.Sprintf("--dns-%s-credentials", provider), + file.Name(), + "-d", + fmt.Sprintf("*.%s", cb.tunnels.domain), + ) + if cb.staging { + cmd.Args = append(cmd.Args, "--staging") + } + cmd.Stdout, cmd.Stderr = os.Stdout, os.Stderr + + if err := cmd.Run(); err != nil { + log.Fatal("Failed to generate SSL certificate") + } +} + // startACMEServer starts a local HTTP server for the ACME challenge. func (cb *CertBot) startACMEServer() *http.Server { mux := http.NewServeMux() diff --git a/gateway/config.go b/gateway/config.go index db8cd081214..f72d6a6bee5 100644 --- a/gateway/config.go +++ b/gateway/config.go @@ -11,18 +11,19 @@ import ( // GatewayConfig holds the configuration settings for the gateway. type GatewayConfig struct { - Env string `env:"SHELLHUB_ENV"` - Domain string `env:"SHELLHUB_DOMAIN,required" validate:"hostname"` - Tunnels bool `env:"SHELLHUB_TUNNELS,default=false"` - TunnelsDomain string `env:"SHELLHUB_TUNNELS_DOMAIN"` - WorkerProcesses string `env:"WORKER_PROCESSES,default=auto"` - MaxWorkerOpenFiles int `env:"MAX_WORKER_OPEN_FILES,default=0"` - MaxWorkerConnections int `env:"MAX_WORKER_CONNECTIONS,default=16384"` - BacklogSize int `env:"BACKLOG_SIZE"` - EnableAutoSSL bool `env:"SHELLHUB_AUTO_SSL"` - EnableProxyProtocol bool `env:"SHELLHUB_PROXY"` - EnableEnterprise bool `env:"SHELLHUB_ENTERPRISE"` - EnableCloud bool `env:"SHELLHUB_CLOUD"` + Env string `env:"SHELLHUB_ENV"` + Domain string `env:"SHELLHUB_DOMAIN,required" validate:"hostname"` + Tunnels bool `env:"SHELLHUB_TUNNELS,default=false"` + TunnelsDomain string `env:"SHELLHUB_TUNNELS_DOMAIN"` + TunnelsDNSProviderToken string `env:"SHELLHUB_TUNNELS_DNS_PROVIDER_TOKEN"` + WorkerProcesses string `env:"WORKER_PROCESSES,default=auto"` + MaxWorkerOpenFiles int `env:"MAX_WORKER_OPEN_FILES,default=0"` + MaxWorkerConnections int `env:"MAX_WORKER_CONNECTIONS,default=16384"` + BacklogSize int `env:"BACKLOG_SIZE"` + EnableAutoSSL bool `env:"SHELLHUB_AUTO_SSL"` + EnableProxyProtocol bool `env:"SHELLHUB_PROXY"` + EnableEnterprise bool `env:"SHELLHUB_ENTERPRISE"` + EnableCloud bool `env:"SHELLHUB_CLOUD"` } var validate = validator.New() diff --git a/gateway/main.go b/gateway/main.go index da9d7c5d786..929d7a914c0 100644 --- a/gateway/main.go +++ b/gateway/main.go @@ -22,6 +22,20 @@ func main() { rootDir: "/etc/letsencrypt", renewedCallback: nginxController.reload, } + + if config.Tunnels { + domain := config.Domain + + if config.TunnelsDomain != "" { + domain = config.TunnelsDomain + } + + certBot.tunnels = &tunnels{ + domain: domain, + token: config.TunnelsDNSProviderToken, + } + } + certBot.ensureCertificates() go certBot.renewCertificates() } diff --git a/gateway/nginx/conf.d/shellhub.conf b/gateway/nginx/conf.d/shellhub.conf index 695ad946e22..4fc116eaa4d 100644 --- a/gateway/nginx/conf.d/shellhub.conf +++ b/gateway/nginx/conf.d/shellhub.conf @@ -647,8 +647,8 @@ server { server { {{ if and ($cfg.EnableAutoSSL) (ne $cfg.Env "development") -}} listen 443; - ssl_certificate /etc/letsencrypt/live/{{ $cfg.Domain }}/fullchain.pem; - ssl_certificate_key /etc/letsencrypt/live/{{ $cfg.Domain }}/privkey.pem; + ssl_certificate "/etc/letsencrypt/live/*.{{ $cfg.Domain }}/fullchain.pem"; + ssl_certificate_key "/etc/letsencrypt/live/*.{{ $cfg.Domain }}/privkey.pem"; ssl_session_cache shared:le_nginx_SSL:10m; ssl_session_timeout 10m;