feat: add security hardening, AppArmor, and ARM64 Raspberry Pi support (Phase 6)
Some checks failed
CI / Go Tests (push) Has been cancelled
CI / Build Go Binaries (amd64, linux, linux-amd64) (push) Has been cancelled
CI / Build Go Binaries (arm64, linux, linux-arm64) (push) Has been cancelled
CI / Shellcheck (push) Has been cancelled

Security hardening: bind kubeconfig server to localhost, mount hardening
(noexec/nosuid/nodev on tmpfs), sysctl network hardening, kernel module
loading lock after boot, SHA256 checksum verification for downloads,
kernel AppArmor + Audit support, complain-mode AppArmor profiles for
containerd and kubelet, and security integration test.

ARM64 Raspberry Pi support: piCore64 base extraction, RPi kernel build
from raspberrypi/linux fork, RPi firmware fetch, SD card image with 4-
partition GPT and tryboot A/B mechanism, BootEnv Go interface abstracting
GRUB vs RPi boot environments, architecture-aware build scripts, QEMU
aarch64 dev VM and boot test.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-12 13:08:17 -06:00
parent 7abf0e0c04
commit efc7f80b65
38 changed files with 2512 additions and 96 deletions

View File

@@ -3,8 +3,6 @@ package cmd
import (
"fmt"
"log/slog"
"github.com/portainer/kubesolo-os/update/pkg/grubenv"
)
// Activate switches the boot target to the passive partition.
@@ -12,7 +10,7 @@ import (
// with boot_counter=3. If health checks fail 3 times, GRUB auto-rolls back.
func Activate(args []string) error {
opts := parseOpts(args)
env := grubenv.New(opts.GrubenvPath)
env := opts.NewBootEnv()
// Get passive slot (the one we want to boot into)
passiveSlot, err := env.PassiveSlot()

View File

@@ -4,7 +4,6 @@ import (
"fmt"
"log/slog"
"github.com/portainer/kubesolo-os/update/pkg/grubenv"
"github.com/portainer/kubesolo-os/update/pkg/image"
"github.com/portainer/kubesolo-os/update/pkg/partition"
)
@@ -18,7 +17,7 @@ func Apply(args []string) error {
return fmt.Errorf("--server is required")
}
env := grubenv.New(opts.GrubenvPath)
env := opts.NewBootEnv()
// Determine passive slot
passiveSlot, err := env.PassiveSlot()

View File

@@ -4,7 +4,6 @@ import (
"fmt"
"log/slog"
"github.com/portainer/kubesolo-os/update/pkg/grubenv"
"github.com/portainer/kubesolo-os/update/pkg/image"
"github.com/portainer/kubesolo-os/update/pkg/partition"
)
@@ -19,7 +18,7 @@ func Check(args []string) error {
}
// Get current version from active partition
env := grubenv.New(opts.GrubenvPath)
env := opts.NewBootEnv()
activeSlot, err := env.ActiveSlot()
if err != nil {
return fmt.Errorf("reading active slot: %w", err)

View File

@@ -5,7 +5,6 @@ import (
"log/slog"
"time"
"github.com/portainer/kubesolo-os/update/pkg/grubenv"
"github.com/portainer/kubesolo-os/update/pkg/health"
)
@@ -15,7 +14,7 @@ import (
// init script) to confirm the system is healthy.
func Healthcheck(args []string) error {
opts := parseOpts(args)
env := grubenv.New(opts.GrubenvPath)
env := opts.NewBootEnv()
// Check if already marked successful
success, err := env.BootSuccess()

View File

@@ -1,11 +1,27 @@
package cmd
import (
"github.com/portainer/kubesolo-os/update/pkg/bootenv"
)
// opts holds shared command-line options for all subcommands.
type opts struct {
ServerURL string
GrubenvPath string
TimeoutSecs int
PubKeyPath string
BootEnvType string // "grub" or "rpi"
BootEnvPath string // path for RPi boot control dir
}
// NewBootEnv creates a BootEnv from the parsed options.
func (o opts) NewBootEnv() bootenv.BootEnv {
switch o.BootEnvType {
case "rpi":
return bootenv.NewRPi(o.BootEnvPath)
default:
return bootenv.NewGRUB(o.GrubenvPath)
}
}
// parseOpts extracts command-line flags from args.
@@ -14,6 +30,7 @@ func parseOpts(args []string) opts {
o := opts{
GrubenvPath: "/boot/grub/grubenv",
TimeoutSecs: 120,
BootEnvType: "grub",
}
for i := 0; i < len(args); i++ {
@@ -46,6 +63,16 @@ func parseOpts(args []string) opts {
o.PubKeyPath = args[i+1]
i++
}
case "--bootenv":
if i+1 < len(args) {
o.BootEnvType = args[i+1]
i++
}
case "--bootenv-path":
if i+1 < len(args) {
o.BootEnvPath = args[i+1]
i++
}
}
}

View File

@@ -3,15 +3,13 @@ package cmd
import (
"fmt"
"log/slog"
"github.com/portainer/kubesolo-os/update/pkg/grubenv"
)
// Rollback forces an immediate switch to the other partition.
// Use this to manually revert to the previous version.
func Rollback(args []string) error {
opts := parseOpts(args)
env := grubenv.New(opts.GrubenvPath)
env := opts.NewBootEnv()
activeSlot, err := env.ActiveSlot()
if err != nil {

View File

@@ -2,42 +2,50 @@ package cmd
import (
"fmt"
"github.com/portainer/kubesolo-os/update/pkg/grubenv"
)
// Status displays the current A/B slot configuration and boot state.
func Status(args []string) error {
opts := parseOpts(args)
env := grubenv.New(opts.GrubenvPath)
env := opts.NewBootEnv()
vars, err := env.ReadAll()
activeSlot, err := env.ActiveSlot()
if err != nil {
return fmt.Errorf("reading GRUB environment: %w", err)
return fmt.Errorf("reading active slot: %w", err)
}
activeSlot := vars["active_slot"]
bootCounter := vars["boot_counter"]
bootSuccess := vars["boot_success"]
passiveSlot, err := env.PassiveSlot()
if err != nil {
return fmt.Errorf("reading passive slot: %w", err)
}
passiveSlot := "B"
if activeSlot == "B" {
passiveSlot = "A"
bootCounter, err := env.BootCounter()
if err != nil {
return fmt.Errorf("reading boot counter: %w", err)
}
bootSuccess, err := env.BootSuccess()
if err != nil {
return fmt.Errorf("reading boot success: %w", err)
}
fmt.Println("KubeSolo OS — A/B Partition Status")
fmt.Println("───────────────────────────────────")
fmt.Printf(" Active slot: %s\n", activeSlot)
fmt.Printf(" Passive slot: %s\n", passiveSlot)
fmt.Printf(" Boot counter: %s\n", bootCounter)
fmt.Printf(" Boot success: %s\n", bootSuccess)
fmt.Printf(" Boot counter: %d\n", bootCounter)
if bootSuccess {
fmt.Printf(" Boot success: 1\n")
} else {
fmt.Printf(" Boot success: 0\n")
}
if bootSuccess == "1" {
if bootSuccess {
fmt.Println("\n ✓ System is healthy (boot confirmed)")
} else if bootCounter == "0" {
} else if bootCounter == 0 {
fmt.Println("\n ✗ Boot counter exhausted — rollback will occur on next reboot")
} else {
fmt.Printf("\n ⚠ Boot pending verification (%s attempts remaining)\n", bootCounter)
fmt.Printf("\n ⚠ Boot pending verification (%d attempts remaining)\n", bootCounter)
}
return nil