Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 14 additions & 15 deletions cmd/cli/commands/network/firewall/add.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,31 +3,30 @@
package firewall

import (
"github.com/joomcode/errorx"
"github.com/spf13/cobra"
)

var addCmd = &cobra.Command{
Use: "add",
Short: "Add a single --mgmt-cidr, --blocked-cidr, or --in-cluster-port",
Short: "Add CIDRs and/or ports to a rule (--name), merging with what is already there",
Long: "Add addresses and/or ports to one rule of the host firewall. --name selects the rule: a reserved block " +
"(mgmt, blocked, in_cluster) or a named allow rule. Adding is idempotent — an entry already present is " +
"left alone.\n\n" +
"The --mgmt-cidr, --blocked-cidr and --in-cluster-port flags are retained shorthands that name their " +
"reserved block implicitly.",
RunE: func(cmd *cobra.Command, _ []string) error {
mgr := newManager()
switch {
case cmd.Flags().Changed("mgmt-cidr"):
return mgr.AddMgmtCIDR(cmd.Context(), flagMgmtCIDR)
case cmd.Flags().Changed("blocked-cidr"):
return mgr.AddBlockedCIDR(cmd.Context(), flagBlockedCIDR)
case cmd.Flags().Changed("in-cluster-port"):
return mgr.AddPort(cmd.Context(), flagInClusterPort)
default:
return errorx.IllegalArgument.New("one of --mgmt-cidr, --blocked-cidr, or --in-cluster-port is required")
name, cidrs, ports, err := resolveTarget(cmd)
if err != nil {
return err
}
return newManager().Add(cmd.Context(), name, cidrs, ports)
},
}

func init() {
addCmd.Flags().StringVar(&flagMgmtCIDR, "mgmt-cidr", "", "A single management CIDR to add")
addCmd.Flags().StringVar(&flagBlockedCIDR, "blocked-cidr", "", "A single operator block-list CIDR to add (dropped inbound, outbound, and forwarded)")
addCmd.Flags().IntVar(&flagInClusterPort, "in-cluster-port", 0, "A single in-cluster host-service port to add")
registerTargetFlags(addCmd, "add")
addCmd.Flags().StringVar(&flagMgmtCIDR, "mgmt-cidr", "", "A single management CIDR to add (shorthand for --name mgmt --cidr)")
addCmd.Flags().StringVar(&flagBlockedCIDR, "blocked-cidr", "", "A single operator block-list CIDR to add (shorthand for --name blocked --cidr)")
addCmd.Flags().IntVar(&flagInClusterPort, "in-cluster-port", 0, "A single in-cluster host-service port to add (shorthand for --name in_cluster --port)")
addCmd.MarkFlagsMutuallyExclusive("mgmt-cidr", "blocked-cidr", "in-cluster-port")
}
158 changes: 101 additions & 57 deletions cmd/cli/commands/network/firewall/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@ package firewall

import (
"context"
"strconv"

"github.com/automa-saga/logx"
"github.com/hashgraph/solo-weaver/cmd/cli/commands/common"
"github.com/hashgraph/solo-weaver/internal/kube"
fw "github.com/hashgraph/solo-weaver/internal/network/firewall"
"github.com/hashgraph/solo-weaver/pkg/sanity"
"github.com/spf13/cobra"
)

Expand All @@ -26,62 +26,17 @@ var detectPodCIDR = func(ctx context.Context) (string, error) {
var createCmd = &cobra.Command{
Use: "create",
Short: "Create the `inet weaver-host-firewall` table (create-if-missing; --force re-renders)",
Long: "Render and apply the full `inet weaver-host-firewall` table. create-if-missing: if the table already " +
"exists, no changes are made unless --force is passed, which re-renders from the flags.",
Long: "Render and apply the full `inet weaver-host-firewall` table, either from flags or from a declarative " +
"config file (--from-file). create-if-missing: if the table already exists, no changes are made unless " +
"--force is passed, which re-renders from the current flags or file.\n\n" +
"--from-file is the only way to declare named allow rules. It is fully declarative: an allow rule absent " +
"from the file is removed. The reserved blocks behave differently — one absent from the file is derived or " +
"defaulted, never removed, so a partial file cannot silently drop management access. To disable a reserved " +
"block, give it an empty address list (`in_cluster: {cidrs: []}`).",
RunE: func(cmd *cobra.Command, args []string) error {
// NewTable() seeds the design defaults (SSH 22, the stack in-cluster
// port set). Override a field only when its flag was explicitly set:
// the flag-binding vars are shared across verbs (see firewall.go), so a
// later verb's registration clobbers another verb's default in the
// shared variable. Reading the shared value unconditionally would wipe
// --in-cluster-ports to nil on a plain `create --force`; gating on
// Changed() keeps NewTable()'s default authoritative.
t := fw.NewTable()
if cmd.Flags().Changed("mgmt-cidrs") {
t.MgmtCIDRs = flagMgmtCIDRs
}
if cmd.Flags().Changed("blocked-cidrs") {
t.BlockedCIDRs = flagBlockedCIDRs
}
if cmd.Flags().Changed("in-cluster-ports") {
t.InClusterPorts = flagInClusterPorts
}
if cmd.Flags().Changed("ssh-port") {
t.SSHPort = flagSSHPort
}

// --pod-cidr accepts a mixed v4/v6 list; route each entry to the matching
// family slot so a dual-stack node can admit in-cluster traffic over both.
// A value that fails family classification is slotted as v4 so Table.Validate
// surfaces a clear --pod-cidr error rather than dropping it silently.
//
// When the operator passes nothing, auto-detection resolves the local
// node's .spec.podCIDR (a single, v4 value today). Detection is
// best-effort — `network firewall create` is node-agnostic and may run
// before a cluster exists, so if no cluster is reachable we fall back to
// omitting the in-cluster-ports rule and tell the operator how to set it.
if len(flagPodCIDR) > 0 {
for _, c := range flagPodCIDR {
if isV6, err := sanity.CIDRIsIPv6(c); err == nil && isV6 {
t.PodCIDR6 = c
} else {
t.PodCIDR = c
}
}
} else {
if cidr, err := detectPodCIDR(cmd.Context()); err != nil {
logx.As().Warn().Err(err).Msg(
"could not auto-detect pod CIDR; the in-cluster host-service ports rule will be omitted — pass --pod-cidr to set it explicitly")
} else {
t.PodCIDR = cidr
logx.As().Info().Str("pod_cidr", cidr).Msg("auto-detected pod CIDR from the local node")
}
}

if len(t.MgmtCIDRs) == 0 {
logx.As().Warn().Msg(
"no --mgmt-cidrs set: the SSH allow rule will match no sources under the default-drop policy — " +
"you will be locked out of new SSH connections; pass --mgmt-cidrs to set the management allowlist")
t, err := buildTable(cmd)
if err != nil {
return err
}

force, err := common.FlagForce().Value(cmd, args)
Expand All @@ -100,10 +55,99 @@ var createCmd = &cobra.Command{
},
}

// buildTable assembles the desired table from --from-file or from the individual
// flags.
func buildTable(cmd *cobra.Command) (*fw.Table, error) {
if cmd.Flags().Changed("from-file") {
cfg, err := fw.LoadConfigFile(flagFromFile)
if err != nil {
return nil, err
}
t, err := cfg.Table()
if err != nil {
return nil, err
}
// An omitted in-cluster block means "use the cluster's pod CIDR"; an
// explicitly empty list means "render no in-cluster rule". Only the former
// triggers detection.
if cfg.InClusterCIDRsUnset() {
applyDetectedPodCIDR(cmd, t)
}
warnOnEmptyMgmt(t)
return t, nil
}

// NewTable() seeds the design defaults (SSH 22, the stack in-cluster port
// set). Override a field only when its flag was explicitly set: the
// flag-binding vars are shared across verbs (see firewall.go), so a later
// verb's registration clobbers another verb's default in the shared
// variable. Reading the shared value unconditionally would wipe
// --in-cluster-ports to nil on a plain `create --force`; gating on Changed()
// keeps NewTable()'s default authoritative.
t := fw.NewTable()
if cmd.Flags().Changed("mgmt-cidrs") {
t.Mgmt.CIDRs = flagMgmtCIDRs
}
if cmd.Flags().Changed("blocked-cidrs") {
t.Blocked.CIDRs = flagBlockedCIDRs
}
if cmd.Flags().Changed("in-cluster-ports") {
t.InCluster.Ports = fw.PortStrings(flagInClusterPorts)
}
if cmd.Flags().Changed("ssh-port") {
t.Mgmt.Ports = []string{strconv.Itoa(flagSSHPort)}
}

// --pod-cidr accepts a mixed v4/v6 list; the renderer routes each entry to
// its family's set, so no slotting is needed here.
//
// When the operator passes nothing, auto-detection resolves the local node's
// .spec.podCIDR. Detection is best-effort — `network firewall create` is
// node-agnostic and may run before a cluster exists — so if no cluster is
// reachable we fall back to omitting the in-cluster rule and tell the
// operator how to set it.
if len(flagPodCIDR) > 0 {
t.InCluster.CIDRs = flagPodCIDR
} else {
applyDetectedPodCIDR(cmd, t)
}

warnOnEmptyMgmt(t)
return t, nil
}

func applyDetectedPodCIDR(cmd *cobra.Command, t *fw.Table) {
cidr, err := detectPodCIDR(cmd.Context())
if err != nil {
logx.As().Warn().Err(err).Msg(
"could not auto-detect pod CIDR; the in-cluster host-service ports rule will be omitted — pass --pod-cidr to set it explicitly")
return
}
t.InCluster.CIDRs = []string{cidr}
logx.As().Info().Str("pod_cidr", cidr).Msg("auto-detected pod CIDR from the local node")
}

func warnOnEmptyMgmt(t *fw.Table) {
if len(t.Mgmt.CIDRs) == 0 {
logx.As().Warn().Msg(
"no management CIDRs set: the management allow rule will match no sources under the default-drop policy — " +
"you will be locked out of new SSH connections; pass --mgmt-cidrs to set the management allowlist")
}
}

func init() {
createCmd.Flags().StringSliceVar(&flagMgmtCIDRs, "mgmt-cidrs", nil, "Management/SSH allowlist CIDRs (comma-separated or repeated)")
createCmd.Flags().StringSliceVar(&flagBlockedCIDRs, "blocked-cidrs", nil, "Operator-curated block list CIDRs, dropped inbound, outbound, and forwarded, ahead of conntrack (comma-separated or repeated)")
createCmd.Flags().StringSliceVar(&flagBlockedCIDRs, "blocked-cidrs", nil, "Operator-curated block list CIDRs, dropped before any other rule (comma-separated or repeated)")
createCmd.Flags().IntSliceVar(&flagInClusterPorts, "in-cluster-ports", fw.DefaultInClusterPorts, "Host-service ports reachable from the pod CIDR")
createCmd.Flags().IntVar(&flagSSHPort, "ssh-port", fw.DefaultSSHPort, "SSH/management TCP port accepted from the allowlist")
createCmd.Flags().StringSliceVar(&flagPodCIDR, "pod-cidr", nil, "Pod CIDR(s) allowed to reach the in-cluster host-service ports; may be IPv4 and/or IPv6 (comma-separated or repeated). Default: auto-detected from the local node's .spec.podCIDR; the rule is omitted if no cluster is reachable")
createCmd.Flags().StringVar(&flagFromFile, "from-file", "", "Declarative YAML config to render the whole table from (the only way to declare named allow rules); mutually exclusive with the individual flags")

// A file states the whole table, so mixing it with a flag that states part of
// one would leave the precedence between them to guesswork.
createCmd.MarkFlagsMutuallyExclusive("from-file", "mgmt-cidrs")
createCmd.MarkFlagsMutuallyExclusive("from-file", "blocked-cidrs")
createCmd.MarkFlagsMutuallyExclusive("from-file", "in-cluster-ports")
createCmd.MarkFlagsMutuallyExclusive("from-file", "ssh-port")
createCmd.MarkFlagsMutuallyExclusive("from-file", "pod-cidr")
}
57 changes: 51 additions & 6 deletions cmd/cli/commands/network/firewall/delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,65 @@ package firewall

import (
"github.com/automa-saga/logx"
"github.com/hashgraph/solo-weaver/cmd/cli/commands/common"
"github.com/hashgraph/solo-weaver/internal/ui/prompt"
"github.com/joomcode/errorx"
"github.com/spf13/cobra"
)

var deleteCmd = &cobra.Command{
Use: "delete",
Short: "Remove the `inet weaver-host-firewall` table and its on-disk artifact",
Long: "Remove the `inet weaver-host-firewall` table and /etc/solo-provisioner/network-weaver-host-firewall.nft. This does NOT " +
"disable the shared solo-provisioner-network-nft.service (shared with `inet weaver-workload-policy`); host-level " +
"teardown is orchestrated by `kube cluster uninstall`.",
RunE: func(cmd *cobra.Command, _ []string) error {
if err := newManager().Delete(cmd.Context()); err != nil {
Short: "Delete one allow rule (--name), or the whole table (--all)",
Long: "Delete a single named allow rule with --name, or tear the whole `inet weaver-host-firewall` table down " +
"with --all (the default when no flag is given, which is what this verb has always done).\n\n" +
"The reserved blocks cannot be deleted individually — clear their addresses instead (`network firewall set " +
"--name mgmt --cidrs \"\"`). --all removes the table and " +
"/etc/solo-provisioner/network-weaver-host-firewall.{nft,yaml}, leaving the host with no weaver-managed " +
"firewall at all, so it asks for confirmation in an interactive session. It does NOT disable the shared " +
"solo-provisioner-network-nft.service (shared with `inet weaver-workload-policy`); host-level teardown is " +
"orchestrated by `kube cluster uninstall`.",
RunE: func(cmd *cobra.Command, args []string) error {
mgr := newManager()

if cmd.Flags().Changed("name") {
if err := mgr.DeleteRule(cmd.Context(), flagName); err != nil {
return err
}
logx.As().Info().Str("rule", flagName).Msg("allow rule removed from the host firewall")
return nil
}

force, err := common.FlagForce().Value(cmd, args)
if err != nil {
return err
}
// No flag at all means --all: that is the behaviour this verb shipped with,
// and the callers relying on it are non-interactive, where ShouldPrompt is
// false and nothing changes for them.
if prompt.ShouldPrompt(force) {
ok, err := prompt.RunConfirm(
"Delete the whole host firewall?",
"This removes the inet weaver-host-firewall table and its on-disk artifacts. The host will have no "+
"weaver-managed firewall — including no management allowlist — until one is created again.",
false)
if err != nil {
return err
}
if !ok {
return errorx.IllegalState.New("aborted: host firewall not deleted")
}
}

if err := mgr.Delete(cmd.Context()); err != nil {
return err
}
logx.As().Info().Msg("inet weaver-host-firewall firewall removed")
return nil
},
}

func init() {
deleteCmd.Flags().StringVar(&flagName, "name", "", "Named allow rule to delete (the reserved blocks cannot be deleted)")
deleteCmd.Flags().BoolVar(&flagAll, "all", false, "Delete the whole table and its on-disk artifacts (the default when --name is omitted)")
deleteCmd.MarkFlagsMutuallyExclusive("name", "all")
}
Loading