🌱🏠 instant least-authority port-forwarding (with automatic HTTPS) for anyone, anywhere! We **really** don't want your TLS private keys, you can keep them 😃 https://greenhouse.server.garden/
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

317 lines
9.4 KiB

package main
import (
"crypto/x509"
"encoding/pem"
"fmt"
"net"
"strings"
"time"
"git.sequentialread.com/forest/greenhouse/pki"
errors "git.sequentialread.com/forest/pkg-errors"
)
const mainCAName = "greenhouse_CA"
const managementAPIAuthCAName = "greenhouse_management_api_auth_CA"
const thresholdCertsDomain = "greenhouse.server.garden"
type ThresholdProvisioningService struct {
BaseHTTPService
PKI *pki.PKIService
}
type ThresholdTunnel struct {
ClientId string
ListenPort int
ListenAddress string
ListenHostnameGlob string
BackEndService string
HaProxyProxyProtocol bool
}
func (tunnel ThresholdTunnel) String() string {
return fmt.Sprintf(
"%s:%d (%s) -> %s (%s,HaProxyProxyProtocol=%t)",
tunnel.ListenAddress, tunnel.ListenPort, tunnel.ListenHostnameGlob, tunnel.ClientId, tunnel.BackEndService, tunnel.HaProxyProxyProtocol,
)
}
type ThresholdClientConfig struct {
ClientId string
GreenhouseDomain string
GreenhouseAPIToken string
ServiceToLocalAddrMap *map[string]string
GreenhouseThresholdPort int
CaCertificate string
ClientTlsKey string
ClientTlsCertificate string
}
func NewThresholdProvisioningService(pkiService *pki.PKIService) *ThresholdProvisioningService {
toReturn := &ThresholdProvisioningService{PKI: pkiService}
// toReturn.ClientFactory = func() (*http.Client, *time.Time, error) {
// clientId := fmt.Sprintf("%d.%s", adminTenantId, adminThresholdNodeId)
// adminTenantClientCertSubject := fmt.Sprintf("%s@%s", clientId, thresholdCertsDomain)
// caCert, err := pkiService.GetCACertificate(mainCAName)
// if err != nil {
// return nil, nil, err
// }
// expiryTime := time.Now().Add(time.Hour * 24)
// cert, err := pkiService.GetClientTLSCertificate(mainCAName, adminTenantClientCertSubject, expiryTime)
// if err != nil {
// return nil, nil, err
// }
// caCertPool := x509.NewCertPool()
// caCertPool.AddCert(caCert)
// tlsClientConfig := &tls.Config{
// Certificates: []tls.Certificate{cert},
// RootCAs: caCertPool,
// }
// tlsClientConfig.BuildNameToCertificate()
// return &http.Client{
// Transport: &http.Transport{
// TLSClientConfig: tlsClientConfig,
// },
// Timeout: 10 * time.Second,
// }, &expiryTime, nil
// }
return toReturn
}
func (service *ThresholdProvisioningService) GetClientConfig(tenantId int, greenhouseDomain, nodeId, apiKey string) (*ThresholdClientConfig, error) {
clientId := fmt.Sprintf("%d.%s", tenantId, nodeId)
certificateSubject := fmt.Sprintf("%s@%s", clientId, thresholdCertsDomain)
mainCA, err := service.PKI.GetCACertificate(mainCAName)
if err != nil {
return nil, errors.Wrap(err, "GetClientConfig(): GetCACertificate")
}
mainCABytes := pem.EncodeToMemory(&pem.Block{
Bytes: mainCA.Raw,
Type: "CERTIFICATE",
})
expiry := time.Now().Add(time.Hour * time.Duration(24*31*12*99))
thresholdKey, thresholdCert, err := service.PKI.GetClientKeyPair(mainCAName, certificateSubject, []string{certificateSubject}, expiry)
if err != nil {
return nil, errors.Wrap(err, "GetClientConfig(): GetClientKeyPair")
}
thresholdKeyBytes := pem.EncodeToMemory(&pem.Block{
Bytes: x509.MarshalPKCS1PrivateKey(thresholdKey),
Type: "RSA PRIVATE KEY",
})
thresholdCertBytes := pem.EncodeToMemory(&pem.Block{
Bytes: thresholdCert.Raw,
Type: "CERTIFICATE",
})
clientConfig := ThresholdClientConfig{
ClientId: clientId,
GreenhouseDomain: greenhouseDomain,
GreenhouseAPIToken: apiKey,
GreenhouseThresholdPort: 9056,
CaCertificate: string(mainCABytes),
ClientTlsKey: string(thresholdKeyBytes),
ClientTlsCertificate: string(thresholdCertBytes),
}
return &clientConfig, nil
}
func (service *ThresholdProvisioningService) GetServerInstallScript(tlsCertificateSubject string, tlsCertificateIps []net.IP) (string, error) {
installScript := `#!/bin/bash
#set -x
set -e
echo "creating threshold user and group"
useradd threshold
#groupadd: group 'threshold' already exists
#groupadd threshold
usermod -a -G threshold threshold
echo "establishing threshold folder"
mkdir /opt/threshold
chown threshold:threshold /opt/threshold
chmod 500 /opt/threshold
echo "downloading threshold tar file"
curl -sS "{{ARTIFACT_URL}}" > /tmp/threshold.gz
# TODO actually verify the checksum here.
#echo "verifying checksum"
#CORRECT_CHECKSUM="$(sha256sum /tmp/threshold-{{ARCH}}.tar.gz | grep '{{TAR_SHA256}}' | wc -l)"
#if [ $CORRECT_CHECKSUM -ne 1 ]; then
# echo "bad checksum on /tmp/threshold-{{ARCH}}.tar.gz:"
# sha256sum /tmp/threshold-{{ARCH}}.tar.gz
# echo "expected {{TAR_SHA256}}."
# exit 1
#fi
echo "unarchiving threshold binary"
gzip --stdout --decompress "/tmp/threshold.gz" > "/opt/threshold/threshold"
chown threshold:threshold /opt/threshold/threshold
chmod 500 /opt/threshold/threshold
echo "use setcap to allow threshold binary to listen on all ports without running as admin"
setcap cap_net_bind_service=+ep /opt/threshold/threshold
echo "writing threshold config file"
echo '
{
"DebugLog": false,
"Domain": "{{THRESHOLD_DOMAIN}}",
"MultiTenantMode": true,
"MultiTenantInternalAPIListenPort": 9057,
"MultiTenantInternalAPICaCertificateFile": "greenhouse_management_api_auth_CA.crt",
"ListenPort": 9056,
"UseTls": true,
"CaCertificateFilesGlob": "greenhouse_CA.crt",
"ServerTlsKeyFile": "threshold.key",
"ServerTlsCertificateFile": "threshold.crt",
"Metrics": {
"PrometheusMetricsAPIPort": 9090
}
}
' > /opt/threshold/config.json
echo "writing x.509 certificate files"
echo '{{GREENHOUSE_MANAGEMENT_API_AUTH_CA}}' > /opt/threshold/greenhouse_management_api_auth_CA.crt
chown threshold:threshold /opt/threshold/greenhouse_management_api_auth_CA.crt
chmod 400 /opt/threshold/greenhouse_management_api_auth_CA.crt
echo '{{GREENHOUSE_CA}}' > /opt/threshold/greenhouse_CA.crt
chown threshold:threshold /opt/threshold/greenhouse_CA.crt
chmod 400 /opt/threshold/greenhouse_CA.crt
echo '{{THRESHOLD_CERT}}' > /opt/threshold/threshold.crt
chown threshold:threshold /opt/threshold/threshold.crt
chmod 400 /opt/threshold/threshold.crt
echo '{{THRESHOLD_KEY}}' > /opt/threshold/threshold.key
chown threshold:threshold /opt/threshold/threshold.key
chmod 400 /opt/threshold/threshold.key
echo "writing threshold systemd service unit file"
echo '
[Unit]
Description=TCP reverse tunnel for greenhouse.server.garden
After=network.target
[Service]
Type=simple
Restart=always
# wait at least 5 seconds before restarting if it crashes
RestartSec=5
# never give up on restarting this service if it crashes
# /etc/systemd/system/threshold.service:14: Unknown lvalue 'StartLimitIntervalSec' i
#StartLimitIntervalSec=0
WorkingDirectory=/opt/threshold
ExecStart=/opt/threshold/threshold -mode server -configFile /opt/threshold/config.json
User=threshold
Group=threshold
[Install]
WantedBy=multi-user.target
' > /etc/systemd/system/threshold.service
chown threshold:threshold /etc/systemd/system/threshold.service
chmod 755 /etc/systemd/system/threshold.service
echo "enabling threshold systemd service"
systemctl daemon-reload
systemctl enable threshold.service
echo "starting threshold systemd service"
systemctl start threshold.service
sleep 1
echo "printing threshold logs:"
echo ""
journalctl -u threshold.service -n 1000 --no-pager
echo ""
echo ""
echo ""
echo "checking threshold service status:"
echo ""
systemctl status threshold.service --no-pager
echo ""
echo ""
echo "threshold has started!"
`
managementAPIAuthCA, err := service.PKI.GetCACertificate(managementAPIAuthCAName)
if err != nil {
return "", errors.Wrap(err, "GetCACertificate")
}
managementAPIAuthCABytes := pem.EncodeToMemory(&pem.Block{
Bytes: managementAPIAuthCA.Raw,
Type: "CERTIFICATE",
})
mainCA, err := service.PKI.GetCACertificate(mainCAName)
if err != nil {
return "", errors.Wrap(err, "GetCACertificate")
}
mainCABytes := pem.EncodeToMemory(&pem.Block{
Bytes: mainCA.Raw,
Type: "CERTIFICATE",
})
expiry := time.Now().Add(time.Hour * time.Duration(24*31*12*99))
thresholdKey, thresholdCert, err := service.PKI.GetServerKeyPair(mainCAName, tlsCertificateSubject, tlsCertificateIps, expiry)
thresholdKeyBytes := pem.EncodeToMemory(&pem.Block{
Bytes: x509.MarshalPKCS1PrivateKey(thresholdKey),
Type: "RSA PRIVATE KEY",
})
thresholdCertBytes := pem.EncodeToMemory(&pem.Block{
Bytes: thresholdCert.Raw,
Type: "CERTIFICATE",
})
substitutions := map[string]string{
"ARTIFACT_URL": "https://picopublish.sequentialread.com/files/threshold-0.0.0-a6ca9d0-6b18-linux-amd64.gz",
"THRESHOLD_SHA256": "0105ea6154aafd711c52626345d0442f3af0ca8a982ff1453b66fd76e7145c0e",
"THRESHOLD_DOMAIN": "greenhouse.server.garden",
"GREENHOUSE_MANAGEMENT_API_AUTH_CA": string(managementAPIAuthCABytes),
"GREENHOUSE_CA": string(mainCABytes),
"THRESHOLD_CERT": string(thresholdCertBytes),
"THRESHOLD_KEY": string(thresholdKeyBytes),
}
for k, v := range substitutions {
installScript = strings.ReplaceAll(installScript, fmt.Sprintf("{{%s}}", k), v)
}
return installScript, nil
}