cli/pkg/unifi/config.go
Snider c9678fc04a Secure SSH and TLS connections, and fix CI issues
Addresses security concerns from OWASP audit and CodeQL by enforcing strict
host key verification and TLS certificate verification.

Security Changes:
- Enforced strict SSH host key checking in pkg/container and devops.
- Removed insecure SSH host key verification from pkg/ansible.
- Added synchronous host key discovery during VM boot using ssh-keyscan.
- Updated UniFi client to enforce TLS certificate verification by default.
- Added --insecure flag and config option for UniFi to allow opt-in to
  skipping TLS verification for self-signed certificates.

CI and Maintenance:
- Fixed auto-merge workflow by providing repository context to 'gh' command.
- Resolved merge conflicts in .github/workflows/auto-merge.yml.
- Added unit tests for secured Ansible SSH client.
- Fixed formatting issues identified by QA checks.
2026-02-05 03:48:42 +00:00

145 lines
4.3 KiB
Go

// Package unifi provides a thin wrapper around the unpoller/unifi Go SDK
// for managing UniFi network controllers, devices, and connected clients.
//
// Authentication is resolved from config file, environment variables, or flag overrides:
//
// 1. ~/.core/config.yaml keys: unifi.url, unifi.user, unifi.pass, unifi.apikey
// 2. UNIFI_URL + UNIFI_USER + UNIFI_PASS + UNIFI_APIKEY environment variables (override config file)
// 3. Flag overrides via core unifi config --url/--user/--pass/--apikey (highest priority)
package unifi
import (
"os"
"github.com/host-uk/core/pkg/config"
"github.com/host-uk/core/pkg/log"
)
const (
// ConfigKeyURL is the config key for the UniFi controller URL.
ConfigKeyURL = "unifi.url"
// ConfigKeyUser is the config key for the UniFi username.
ConfigKeyUser = "unifi.user"
// ConfigKeyPass is the config key for the UniFi password.
ConfigKeyPass = "unifi.pass"
// ConfigKeyAPIKey is the config key for the UniFi API key.
ConfigKeyAPIKey = "unifi.apikey"
// ConfigKeyInsecure is the config key for allowing insecure TLS connections.
ConfigKeyInsecure = "unifi.insecure"
// DefaultURL is the default UniFi controller URL.
DefaultURL = "https://10.69.1.1"
)
// NewFromConfig creates a UniFi client using the standard config resolution:
//
// 1. ~/.core/config.yaml keys: unifi.url, unifi.user, unifi.pass, unifi.apikey, unifi.insecure
// 2. UNIFI_URL + UNIFI_USER + UNIFI_PASS + UNIFI_APIKEY + UNIFI_INSECURE environment variables
// 3. Provided flag overrides (highest priority)
func NewFromConfig(flagURL, flagUser, flagPass, flagAPIKey string, flagInsecure bool) (*Client, error) {
url, user, pass, apikey, insecure, err := ResolveConfig(flagURL, flagUser, flagPass, flagAPIKey, flagInsecure)
if err != nil {
return nil, err
}
if user == "" && apikey == "" {
return nil, log.E("unifi.NewFromConfig", "no credentials configured (set UNIFI_USER/UNIFI_PASS or UNIFI_APIKEY, or run: core unifi config)", nil)
}
return New(url, user, pass, apikey, insecure)
}
// ResolveConfig resolves the UniFi URL and credentials from all config sources.
// Flag values take highest priority, then env vars, then config file.
func ResolveConfig(flagURL, flagUser, flagPass, flagAPIKey string, flagInsecure bool) (url, user, pass, apikey string, insecure bool, err error) {
// Start with config file values
cfg, cfgErr := config.New()
if cfgErr == nil {
_ = cfg.Get(ConfigKeyURL, &url)
_ = cfg.Get(ConfigKeyUser, &user)
_ = cfg.Get(ConfigKeyPass, &pass)
_ = cfg.Get(ConfigKeyAPIKey, &apikey)
_ = cfg.Get(ConfigKeyInsecure, &insecure)
}
// Overlay environment variables
if envURL := os.Getenv("UNIFI_URL"); envURL != "" {
url = envURL
}
if envUser := os.Getenv("UNIFI_USER"); envUser != "" {
user = envUser
}
if envPass := os.Getenv("UNIFI_PASS"); envPass != "" {
pass = envPass
}
if envAPIKey := os.Getenv("UNIFI_APIKEY"); envAPIKey != "" {
apikey = envAPIKey
}
if os.Getenv("UNIFI_INSECURE") == "true" {
insecure = true
}
// Overlay flag values (highest priority)
if flagURL != "" {
url = flagURL
}
if flagUser != "" {
user = flagUser
}
if flagPass != "" {
pass = flagPass
}
if flagAPIKey != "" {
apikey = flagAPIKey
}
if flagInsecure {
insecure = true
}
// Default URL if nothing configured
if url == "" {
url = DefaultURL
}
return url, user, pass, apikey, insecure, nil
}
// SaveConfig persists the UniFi URL and/or credentials to the config file.
func SaveConfig(url, user, pass, apikey string, insecure *bool) error {
cfg, err := config.New()
if err != nil {
return log.E("unifi.SaveConfig", "failed to load config", err)
}
if url != "" {
if err := cfg.Set(ConfigKeyURL, url); err != nil {
return log.E("unifi.SaveConfig", "failed to save URL", err)
}
}
if user != "" {
if err := cfg.Set(ConfigKeyUser, user); err != nil {
return log.E("unifi.SaveConfig", "failed to save user", err)
}
}
if pass != "" {
if err := cfg.Set(ConfigKeyPass, pass); err != nil {
return log.E("unifi.SaveConfig", "failed to save password", err)
}
}
if apikey != "" {
if err := cfg.Set(ConfigKeyAPIKey, apikey); err != nil {
return log.E("unifi.SaveConfig", "failed to save API key", err)
}
}
if insecure != nil {
if err := cfg.Set(ConfigKeyInsecure, *insecure); err != nil {
return log.E("unifi.SaveConfig", "failed to save insecure setting", err)
}
}
return nil
}