Single Go binary with five commands: - build: podman build locally with registry tags + git version - push: podman push to MCR - deploy: SSH pull/stop/rm/run on target node - cert renew: issue TLS cert from Metacrypt via REST API - status: show container status on a node Config-driven via TOML service registry describing images, Dockerfiles, container configs per node. Shells out to podman for container operations and ssh for remote access. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
216 lines
5.4 KiB
Go
216 lines
5.4 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"os"
|
|
"strings"
|
|
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
func certCommand() *cobra.Command {
|
|
cmd := &cobra.Command{
|
|
Use: "cert",
|
|
Short: "Certificate management",
|
|
}
|
|
|
|
cmd.AddCommand(certRenewCommand())
|
|
return cmd
|
|
}
|
|
|
|
func certRenewCommand() *cobra.Command {
|
|
var installFlag string
|
|
var caCertFlag string
|
|
|
|
cmd := &cobra.Command{
|
|
Use: "renew <hostname>",
|
|
Short: "Request a new certificate from Metacrypt",
|
|
Args: cobra.ExactArgs(1),
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
hostname := args[0]
|
|
|
|
addr := os.Getenv("METACRYPT_ADDR")
|
|
if addr == "" {
|
|
addr = "https://metacrypt.svc.mcp.metacircular.net:8443"
|
|
}
|
|
token := os.Getenv("METACRYPT_TOKEN")
|
|
if token == "" {
|
|
return fmt.Errorf("METACRYPT_TOKEN environment variable is required")
|
|
}
|
|
|
|
// Build request body.
|
|
reqBody := map[string]any{
|
|
"mount": "pki",
|
|
"operation": "issue",
|
|
"path": "web",
|
|
"data": map[string]any{
|
|
"issuer": "web",
|
|
"common_name": hostname,
|
|
"profile": "server",
|
|
"dns_names": []string{hostname},
|
|
"ttl": "2160h",
|
|
},
|
|
}
|
|
bodyBytes, err := json.Marshal(reqBody)
|
|
if err != nil {
|
|
return fmt.Errorf("marshal request: %w", err)
|
|
}
|
|
|
|
// Configure TLS.
|
|
tlsCfg := &tls.Config{
|
|
MinVersion: tls.VersionTLS13,
|
|
}
|
|
if caCertFlag != "" {
|
|
caPEM, err := os.ReadFile(caCertFlag)
|
|
if err != nil {
|
|
return fmt.Errorf("read CA cert: %w", err)
|
|
}
|
|
pool := x509.NewCertPool()
|
|
if !pool.AppendCertsFromPEM(caPEM) {
|
|
return fmt.Errorf("failed to parse CA cert")
|
|
}
|
|
tlsCfg.RootCAs = pool
|
|
} else {
|
|
tlsCfg.InsecureSkipVerify = true
|
|
}
|
|
|
|
client := &http.Client{
|
|
Transport: &http.Transport{
|
|
TLSClientConfig: tlsCfg,
|
|
},
|
|
}
|
|
|
|
url := addr + "/v1/engine/request"
|
|
fmt.Printf(">>> POST %s\n", url)
|
|
|
|
req, err := http.NewRequest("POST", url, bytes.NewReader(bodyBytes))
|
|
if err != nil {
|
|
return fmt.Errorf("create request: %w", err)
|
|
}
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set("Authorization", "Bearer "+token)
|
|
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return fmt.Errorf("request failed: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
respBody, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return fmt.Errorf("read response: %w", err)
|
|
}
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
return fmt.Errorf("metacrypt returned %d: %s", resp.StatusCode, string(respBody))
|
|
}
|
|
|
|
var result map[string]any
|
|
if err := json.Unmarshal(respBody, &result); err != nil {
|
|
return fmt.Errorf("parse response: %w", err)
|
|
}
|
|
|
|
certPEM, _ := result["cert_pem"].(string)
|
|
chainPEM, _ := result["chain_pem"].(string)
|
|
keyPEM, _ := result["key_pem"].(string)
|
|
|
|
if certPEM == "" || keyPEM == "" {
|
|
return fmt.Errorf("response missing cert_pem or key_pem")
|
|
}
|
|
|
|
// Combine leaf and chain into full cert.
|
|
fullCert := certPEM
|
|
if chainPEM != "" {
|
|
fullCert = certPEM + "\n" + chainPEM
|
|
}
|
|
|
|
// Parse and display cert details.
|
|
tlsCert, err := tls.X509KeyPair([]byte(fullCert), []byte(keyPEM))
|
|
if err != nil {
|
|
return fmt.Errorf("parse cert: %w", err)
|
|
}
|
|
leaf, err := x509.ParseCertificate(tlsCert.Certificate[0])
|
|
if err != nil {
|
|
return fmt.Errorf("parse leaf cert: %w", err)
|
|
}
|
|
|
|
fmt.Printf("Certificate issued:\n")
|
|
fmt.Printf(" Serial: %s\n", leaf.SerialNumber.Text(16))
|
|
fmt.Printf(" CN: %s\n", leaf.Subject.CommonName)
|
|
fmt.Printf(" Expires: %s\n", leaf.NotAfter.Format("2006-01-02 15:04:05 UTC"))
|
|
|
|
// Install to remote node if requested.
|
|
if installFlag != "" {
|
|
parts := strings.SplitN(installFlag, ":", 2)
|
|
if len(parts) != 2 {
|
|
return fmt.Errorf("--install must be <node>:<path> (e.g. mcp:/srv/mc-proxy/tls)")
|
|
}
|
|
node := parts[0]
|
|
remotePath := parts[1]
|
|
|
|
cfg, err := loadCfg()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
nodeCfg, err := cfg.FindNode(node)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
host := nodeCfg.Host
|
|
if nodeCfg.User != "" {
|
|
host = nodeCfg.User + "@" + nodeCfg.Host
|
|
}
|
|
|
|
// Write temp files and SCP them.
|
|
certFile, err := os.CreateTemp("", "cert-*.pem")
|
|
if err != nil {
|
|
return fmt.Errorf("create temp cert: %w", err)
|
|
}
|
|
defer os.Remove(certFile.Name())
|
|
|
|
keyFile, err := os.CreateTemp("", "key-*.pem")
|
|
if err != nil {
|
|
return fmt.Errorf("create temp key: %w", err)
|
|
}
|
|
defer os.Remove(keyFile.Name())
|
|
|
|
if _, err := certFile.WriteString(fullCert); err != nil {
|
|
return fmt.Errorf("write temp cert: %w", err)
|
|
}
|
|
certFile.Close()
|
|
|
|
if _, err := keyFile.WriteString(keyPEM); err != nil {
|
|
return fmt.Errorf("write temp key: %w", err)
|
|
}
|
|
keyFile.Close()
|
|
|
|
certDest := host + ":" + remotePath + "/cert.pem"
|
|
keyDest := host + ":" + remotePath + "/key.pem"
|
|
|
|
if err := run("scp", certFile.Name(), certDest); err != nil {
|
|
return fmt.Errorf("scp cert: %w", err)
|
|
}
|
|
if err := run("scp", keyFile.Name(), keyDest); err != nil {
|
|
return fmt.Errorf("scp key: %w", err)
|
|
}
|
|
|
|
fmt.Printf("Installed cert and key to %s:%s/\n", node, remotePath)
|
|
}
|
|
|
|
return nil
|
|
},
|
|
}
|
|
|
|
cmd.Flags().StringVar(&installFlag, "install", "", "install cert to <node>:<path>")
|
|
cmd.Flags().StringVar(&caCertFlag, "ca-cert", "", "CA certificate for Metacrypt TLS verification")
|
|
return cmd
|
|
}
|