Files
the-other-dude/poller/internal/device/cert_deploy.go
Jason Staack b840047e19 feat: The Other Dude v9.0.1 — full-featured email system
ci: add GitHub Pages deployment workflow for docs site

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 19:30:44 -05:00

123 lines
4.6 KiB
Go

// Package device provides the full certificate deployment flow for RouterOS devices.
//
// The deployment follows these steps:
// 1. Upload cert.pem and key.pem via SFTP
// 2. Import the certificate via RouterOS API (/certificate/import)
// 3. Import the private key via RouterOS API (/certificate/import)
// 4. Determine the certificate name on device
// 5. Assign the certificate to the api-ssl service (/ip/service/set)
// 6. Clean up uploaded PEM files from device filesystem (/file/remove)
package device
import (
"fmt"
"log/slog"
routeros "github.com/go-routeros/routeros/v3"
"golang.org/x/crypto/ssh"
)
// CertDeployRequest is the NATS request payload for certificate deployment.
type CertDeployRequest struct {
DeviceID string `json:"device_id"`
CertPEM string `json:"cert_pem"`
KeyPEM string `json:"key_pem"`
CertName string `json:"cert_name"` // e.g., "portal-device-cert"
SSHPort int `json:"ssh_port"`
}
// CertDeployResponse is the NATS reply payload.
type CertDeployResponse struct {
Success bool `json:"success"`
CertNameOnDevice string `json:"cert_name_on_device,omitempty"`
Error string `json:"error,omitempty"`
}
// DeployCert performs the full certificate deployment flow:
// 1. Upload cert.pem and key.pem files via SFTP
// 2. Import certificate via RouterOS API
// 3. Import key via RouterOS API
// 4. Assign certificate to api-ssl service
// 5. Clean up uploaded PEM files from device filesystem
func DeployCert(sshClient *ssh.Client, apiClient *routeros.Client, req CertDeployRequest) CertDeployResponse {
certFile := req.CertName + ".pem"
keyFile := req.CertName + "-key.pem"
// Step 1: Upload cert via SFTP
slog.Debug("uploading cert file via SFTP", "file", certFile, "device_id", req.DeviceID)
if err := UploadFile(sshClient, certFile, []byte(req.CertPEM)); err != nil {
return CertDeployResponse{Success: false, Error: fmt.Sprintf("SFTP cert upload: %s", err)}
}
// Step 2: Upload key via SFTP
slog.Debug("uploading key file via SFTP", "file", keyFile, "device_id", req.DeviceID)
if err := UploadFile(sshClient, keyFile, []byte(req.KeyPEM)); err != nil {
return CertDeployResponse{Success: false, Error: fmt.Sprintf("SFTP key upload: %s", err)}
}
// Step 3: Import certificate
slog.Debug("importing certificate", "file", certFile, "device_id", req.DeviceID)
importResult := ExecuteCommand(apiClient, "/certificate/import", []string{
"=file-name=" + certFile,
})
if !importResult.Success {
return CertDeployResponse{Success: false, Error: fmt.Sprintf("cert import: %s", importResult.Error)}
}
// Step 4: Import private key
slog.Debug("importing private key", "file", keyFile, "device_id", req.DeviceID)
keyImportResult := ExecuteCommand(apiClient, "/certificate/import", []string{
"=file-name=" + keyFile,
})
if !keyImportResult.Success {
return CertDeployResponse{Success: false, Error: fmt.Sprintf("key import: %s", keyImportResult.Error)}
}
// Determine the certificate name on device.
// RouterOS names imported certs as <filename>_0 by convention.
// Query to find the actual name by looking for certs with a private key.
certNameOnDevice := certFile + "_0"
printResult := ExecuteCommand(apiClient, "/certificate/print", []string{
"=.proplist=name,common-name,private-key",
})
if printResult.Success && len(printResult.Data) > 0 {
// Use the last cert that has a private key (most recently imported)
for _, entry := range printResult.Data {
if name, ok := entry["name"]; ok {
if pk, hasPK := entry["private-key"]; hasPK && pk == "true" {
certNameOnDevice = name
}
}
}
}
// Step 5: Assign to api-ssl service
slog.Debug("assigning certificate to api-ssl", "cert_name", certNameOnDevice, "device_id", req.DeviceID)
assignResult := ExecuteCommand(apiClient, "/ip/service/set", []string{
"=numbers=api-ssl",
"=certificate=" + certNameOnDevice,
})
if !assignResult.Success {
slog.Warn("api-ssl assignment failed (cert still imported)",
"device_id", req.DeviceID,
"error", assignResult.Error,
)
// Don't fail entirely -- cert is imported, assignment can be retried
}
// Step 6: Clean up uploaded PEM files from device filesystem
slog.Debug("cleaning up PEM files", "device_id", req.DeviceID)
ExecuteCommand(apiClient, "/file/remove", []string{"=.id=" + certFile})
ExecuteCommand(apiClient, "/file/remove", []string{"=.id=" + keyFile})
// File cleanup failures are non-fatal
slog.Info("certificate deployed successfully",
"device_id", req.DeviceID,
"cert_name", certNameOnDevice,
)
return CertDeployResponse{
Success: true,
CertNameOnDevice: certNameOnDevice,
}
}