Skip to content

Commit

Permalink
feat(gateway): generate certificates for tunnels using DigitalOcean DNS
Browse files Browse the repository at this point in the history
  • Loading branch information
henrybarreto committed Dec 11, 2024
1 parent 4fd1cde commit 1c37f18
Show file tree
Hide file tree
Showing 7 changed files with 90 additions and 18 deletions.
8 changes: 7 additions & 1 deletion .env
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down
6 changes: 3 additions & 3 deletions gateway/Dockerfile
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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

Expand Down
50 changes: 50 additions & 0 deletions gateway/certbot.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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.
Expand Down Expand Up @@ -65,6 +84,37 @@ 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)
file, _ := os.Create(fmt.Sprintf("/etc/shellhub-gateway/%s.ini", 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()
Expand Down
25 changes: 13 additions & 12 deletions gateway/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
14 changes: 14 additions & 0 deletions gateway/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
Expand Down
4 changes: 2 additions & 2 deletions gateway/nginx/conf.d/shellhub.conf
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down

0 comments on commit 1c37f18

Please sign in to comment.