Skip to content

Commit f9ce5ee

Browse files
authored
feat: add extra sans support for the api server (#32)
1 parent 20a2e75 commit f9ce5ee

5 files changed

Lines changed: 61 additions & 4 deletions

File tree

cmd/kubesolo/main.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"os/signal"
77
"path/filepath"
88
rdebug "runtime/debug"
9+
"strings"
910
"syscall"
1011

1112
"github.com/alecthomas/kingpin/v2"
@@ -36,6 +37,7 @@ var (
3637
// the main struct for the kubesolo application
3738
type kubesolo struct {
3839
hostName string
40+
extraSANs string
3941
debug bool
4042
pprofServer bool
4143
portainerEdgeID string
@@ -59,6 +61,7 @@ var (
5961
func service() (*kubesolo, error) {
6062
return &kubesolo{
6163
hostName: system.GetHostname(),
64+
extraSANs: *flags.APIServerExtraSANs,
6265
debug: *flags.Debug,
6366
pprofServer: *flags.PprofServer,
6467
portainerEdgeID: *flags.PortainerEdgeID,
@@ -334,6 +337,8 @@ func (s *kubesolo) bootstrap() {
334337
// API Server paths
335338
APIServerDir: filepath.Join(basePath, types.DefaultAPIServerDir),
336339
ServiceAccountKeyFile: filepath.Join(basePath, types.DefaultPKIDir, "apiserver", "service-account.key"),
340+
// API Server extra SANs
341+
APIServerExtraSANs: strings.Split(s.extraSANs, ","),
337342

338343
// Kine paths
339344
KineDir: filepath.Join(basePath, types.KubesoloKineDir),

internal/config/flags/flags.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import "github.com/alecthomas/kingpin/v2"
44

55
// the full list of flags for the kubesolo application
66
// Path is the path to the directory containing the kubesolo configuration files
7+
// APIServerExtraSANs is the flag to add extra SANs to the API Server certificate
78
// PortainerEdgeID is the Edge ID for the Portainer Edge Agent
89
// PortainerEdgeKey is the Edge Key for the Portainer Edge Agent that can be used to register the Edge Agent with the Portainer Server
910
// PortainerEdgeAsync is the flag to enable Portainer Edge Async Mode
@@ -13,6 +14,7 @@ import "github.com/alecthomas/kingpin/v2"
1314
var (
1415
Application = kingpin.New("kubesolo", "Ultra-lightweight, OCI-compliant, single-node Kubernetes built for constrained environments such as IoT or IIoT devices running in embedded environments.")
1516
Path = Application.Flag("path", "Path to the directory containing the kubesolo configuration files. Defaults to /var/lib/kubesolo.").Envar("KUBESOLO_PATH").Default("/var/lib/kubesolo").String()
17+
APIServerExtraSANs = Application.Flag("apiserver-extra-sans", "A comma-separated list of additional Subject Alternative Names (SANs) to include in the API server's TLS certificate. These SANs can be IP addresses or DNS names (e.g., 10.0.0.4,kubesolo.local).").Envar("KUBESOLO_APISERVER_EXTRA_SANS").Default("").String()
1618
PortainerEdgeID = Application.Flag("portainer-edge-id", "Portainer Edge ID. Defaults to empty string.").Envar("KUBESOLO_PORTAINER_EDGE_ID").Default("").String()
1719
PortainerEdgeKey = Application.Flag("portainer-edge-key", "Portainer Edge Key. Defaults to empty string.").Envar("KUBESOLO_PORTAINER_EDGE_KEY").Default("").String()
1820
PortainerEdgeAsync = Application.Flag("portainer-edge-async", "Enable Portainer Edge Async Mode. Defaults to false.").Envar("KUBESOLO_PORTAINER_EDGE_ASYNC").Default("false").Bool()

internal/core/pki/options.go

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@ import (
44
"fmt"
55
"net"
66
"path/filepath"
7+
"strings"
78

89
"github.com/portainer/kubesolo/internal/runtime/network"
910
"github.com/portainer/kubesolo/internal/system"
1011
"github.com/portainer/kubesolo/types"
12+
"github.com/rs/zerolog/log"
1113
)
1214

1315
// defaultCertOptions returns default options for the specified certificate type
@@ -59,11 +61,11 @@ func defaultCertOptions(certType CertificateType, embedded types.Embedded) CertO
5961
"kubernetes.default.svc.cluster.local",
6062
"localhost",
6163
}
62-
opts.IPAddresses = []net.IP{
63-
net.ParseIP("127.0.0.1"),
64-
net.ParseIP(types.DefaultKubernetesServiceIP),
65-
}
64+
opts.IPAddresses = []net.IP{net.ParseIP(types.DefaultKubernetesServiceIP)}
6665
opts.IPAddresses = append(opts.IPAddresses, ipAddresses...)
66+
if len(embedded.APIServerExtraSANs) > 0 {
67+
addExtraSANs(&opts, embedded.APIServerExtraSANs)
68+
}
6769
opts.SignerCertDir = embedded.CACerts.Cert
6870
opts.SignerKeyDir = embedded.CACerts.Key
6971
opts.CertDir = filepath.Join(embedded.PKIAPIServerDir, "apiserver.crt")
@@ -100,3 +102,26 @@ func defaultCertOptions(certType CertificateType, embedded types.Embedded) CertO
100102

101103
return opts
102104
}
105+
106+
// addExtraSANs adds extra SANs to the certificate options
107+
// it validates the SANs and adds them to the certificate options
108+
// if the SAN is a valid IPv4 address, it adds it to the IPAddresses field
109+
// if the SAN is a valid DNS name, it adds it to the DNSNames field
110+
// if the SAN is invalid, it logs a warning and skips it
111+
func addExtraSANs(opts *CertOptions, extraSANs []string) {
112+
for _, san := range extraSANs {
113+
san = strings.TrimSpace(san)
114+
if san == "" {
115+
continue
116+
}
117+
118+
if network.IsIPv4Address(san) {
119+
opts.IPAddresses = append(opts.IPAddresses, net.ParseIP(san))
120+
} else if network.IsDNSName(san) {
121+
opts.DNSNames = append(opts.DNSNames, san)
122+
} else {
123+
log.Warn().Str("component", "pki").Msgf("invalid SAN: %s", san)
124+
continue
125+
}
126+
}
127+
}

internal/runtime/network/ip.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@ package network
33
import (
44
"fmt"
55
"net"
6+
"regexp"
7+
)
8+
9+
var (
10+
rfc1123 = `^[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?)*$`
611
)
712

813
// GetLocalIPs returns all non-loopback IPv4 addresses
@@ -38,3 +43,20 @@ func GetNodeIP() (string, error) {
3843

3944
return "127.0.0.1", fmt.Errorf("could not find non-loopback private IP address")
4045
}
46+
47+
// IsIPv4Address returns true if the given string is a valid IPv4 address
48+
func IsIPv4Address(ip string) bool {
49+
return net.ParseIP(ip) != nil && net.ParseIP(ip).To4() != nil
50+
}
51+
52+
// IsDNSName returns true if the given string is a valid DNS name
53+
// it follows the RFC 1123 specification for DNS names
54+
func IsDNSName(name string) bool {
55+
if name == "" || len(name) > 253 {
56+
return false
57+
}
58+
59+
// RFC 1123 compliant DNS name pattern
60+
matched, _ := regexp.MatchString(rfc1123, name)
61+
return matched
62+
}

types/types.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ type Embedded struct {
7171
ContainerdShimBinaryFile string
7272
ContainerdRootDir string
7373
ContainerdStateDir string
74+
7475
// Conitainerd CNI directories and files
7576
ContainerdCNIDir string
7677
ContainerdCNIPluginsDir string
@@ -90,6 +91,8 @@ type Embedded struct {
9091
// API Server directory
9192
APIServerDir string
9293
ServiceAccountKeyFile string
94+
// API Server extra SANs
95+
APIServerExtraSANs []string
9396

9497
// Kine directories and files
9598
KineDir string

0 commit comments

Comments
 (0)