Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
44 changes: 44 additions & 0 deletions internal/guest/runtime/hcsv2/network.go
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,50 @@ type nicInNamespace struct {
assignedPid int
}

// ConfigureInInitNS configures all adapters in the current (init) network
// namespace without moving them. This is used for host-network containers
// where all containers share the init netns and the NIC must remain there.
func (n *namespace) ConfigureInInitNS(ctx context.Context) (err error) {
ctx, span := ot.StartSpan(ctx, "namespace::ConfigureInInitNS")
defer span.End()
defer func() { ot.SetSpanStatus(span, err) }()
span.SetAttributes(attribute.String("namespace", n.id))

n.m.Lock()
defer n.m.Unlock()

// Mirror Sync()'s multi-NIC handling so host-network and non-host-network
// pods behave identically with respect to policy-based routing.
for i, a := range n.nics {
if a.adapter.PolicyBasedRouting {
a.adapter.EnableLowMetric = (i > 0)
}
if err = a.configureInPlace(ctx); err != nil {
return err
}
}
return nil
}

// configureInPlace configures the adapter in the current network namespace
// (no move). It applies IP addresses, routes, and gateway settings just like
// assignToPid but without calling MoveInterfaceToNS or switching netns.
func (nin *nicInNamespace) configureInPlace(ctx context.Context) (err error) {
ctx, span := ot.StartSpan(ctx, "nicInNamespace::configureInPlace")
defer span.End()
defer func() { ot.SetSpanStatus(span, err) }()
span.SetAttributes(
attribute.String("adapterID", nin.adapter.ID),
attribute.String("ifname", nin.ifname))

// Configure directly in the current (init) namespace — no move needed.
// Use PID 1 as the "nsPid" parameter for NetNSConfig logging context.
if err := network.NetNSConfig(ctx, nin.ifname, 1, nin.adapter); err != nil {
return errors.Wrapf(err, "failed to configure adapter aid: %s, if id: %s in init netns", nin.adapter.ID, nin.ifname)
}
return nil
}

// assignToPid assigns `nin.adapter`, represented by `nin.ifname` to `pid`.
func (nin *nicInNamespace) assignToPid(ctx context.Context, pid int) (err error) {
ctx, span := ot.StartSpan(ctx, "nicInNamespace::assignToPid")
Expand Down
53 changes: 47 additions & 6 deletions internal/guest/runtime/hcsv2/uvm.go
Original file line number Diff line number Diff line change
Expand Up @@ -744,6 +744,36 @@ func (h *Host) CreateContainer(ctx context.Context, id string, settings *prot.VM
}
}

// Determine hostNetwork mode. For sandbox/standalone containers, check their
// own annotations. For workload containers, inherit from the sandbox so that
// only pod.snp.json needs the LCOWHostNetwork annotation — container.json
// does not need it (and the value is ignored if present).
var hostNetwork bool
if criType == "sandbox" || !isCRI {
hostNetwork = oci.ParseAnnotationsBool(ctx, settings.OCISpecification.Annotations, annotations.LCOWHostNetwork, false)
} else if criType == "container" {
h.containersMutex.Lock()
if sandbox, ok := h.containers[sandboxID]; ok {
hostNetwork = oci.ParseAnnotationsBool(ctx, sandbox.spec.Annotations, annotations.LCOWHostNetwork, false)
}
h.containersMutex.Unlock()
}
if hostNetwork {
if err := h.securityOptions.PolicyEnforcer.EnforceHostNetworkPolicy(ctx); err != nil {
return nil, err
}
}
if hostNetwork && settings.OCISpecification.Linux != nil {
filtered := settings.OCISpecification.Linux.Namespaces[:0]
for _, ns := range settings.OCISpecification.Linux.Namespaces {
if ns.Type != specs.NetworkNamespace {
filtered = append(filtered, ns)
}
}
settings.OCISpecification.Linux.Namespaces = filtered
log.G(ctx).WithField("cid", id).Info("Host network namespace enabled: removed network namespace from OCI spec")
}

// Create the BundlePath
if err := os.MkdirAll(settings.OCIBundlePath, 0700); err != nil {
return nil, errors.Wrapf(err, "failed to create OCIBundlePath: '%s'", settings.OCIBundlePath)
Expand All @@ -765,7 +795,7 @@ func (h *Host) CreateContainer(ctx context.Context, id string, settings *prot.VM
c.container = con
c.initProcess = newProcess(c, settings.OCISpecification.Process, init, uint32(c.container.Pid()), true)

// Sandbox or standalone, move the networks to the container namespace
// Sandbox or standalone, set up networks.
if criType == "sandbox" || !isCRI {
ns, err := getNetworkNamespace(namespaceID)
// skip network activity for sandbox containers marked with skip uvm networking annotation
Expand All @@ -774,11 +804,22 @@ func (h *Host) CreateContainer(ctx context.Context, id string, settings *prot.VM
}
// standalone is not required to have a networking namespace setup
if ns != nil {
if err := ns.AssignContainerPid(ctx, c.container.Pid()); err != nil {
return nil, err
}
if err := ns.Sync(ctx); err != nil {
return nil, err
if hostNetwork {
// Host network mode: configure adapters in-place in the init
// network namespace (do NOT move them). This keeps eth0 visible
// to all containers sharing the init netns while still setting
// up IP addresses, routes, and gateways.
if err := ns.ConfigureInInitNS(ctx); err != nil {
return nil, err
}
} else {
// Normal mode: move adapters into the sandbox's network namespace.
if err := ns.AssignContainerPid(ctx, c.container.Pid()); err != nil {
return nil, err
}
if err := ns.Sync(ctx); err != nil {
return nil, err
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ external_processes := [
allow_properties_access := false
allow_dump_stacks := false
allow_runtime_logging := false
allow_host_network := false
allow_environment_variable_dropping := false
allow_unencrypted_scratch := false
allow_capability_dropping := true
Expand All @@ -77,6 +78,7 @@ plan9_unmount := data.framework.plan9_unmount
get_properties := data.framework.get_properties
dump_stacks := data.framework.dump_stacks
runtime_logging := data.framework.runtime_logging
host_network := data.framework.host_network
load_fragment := data.framework.load_fragment
scratch_mount := data.framework.scratch_mount
scratch_unmount := data.framework.scratch_unmount
Expand Down
1 change: 1 addition & 0 deletions internal/tools/securitypolicy/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ func main() {
config.AllowPropertiesAccess,
config.AllowDumpStacks,
config.AllowRuntimeLogging,
config.AllowHostNetwork,
config.AllowEnvironmentVariableDropping,
config.AllowUnencryptedScratch,
config.AllowCapabilityDropping,
Expand Down
6 changes: 6 additions & 0 deletions pkg/annotations/annotations.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,12 @@ const (
// LCOWPrivileged is used to specify that the container should be run in privileged mode.
LCOWPrivileged = "io.microsoft.virtualmachine.lcow.privileged"

// LCOWHostNetwork specifies that the container should share the UVM's (host) network
// namespace instead of creating its own. This is required for workloads that need
// to communicate with the kernel via network-namespace-scoped netlink sockets
// (e.g., iSCSI initiator using NETLINK_ISCSI).
LCOWHostNetwork = "io.microsoft.virtualmachine.lcow.hostnetwork"

// LCOWTeeLogPath specifies a path in the Linux uVM to write container's stdio to,
// in addition to the usual vsock pipes.
//
Expand Down
1 change: 1 addition & 0 deletions pkg/securitypolicy/api.rego
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,5 @@ enforcement_points := {
"scratch_mount": {"introducedVersion": "0.10.0", "default_results": {"allowed": true}, "use_framework": false},
"scratch_unmount": {"introducedVersion": "0.10.0", "default_results": {"allowed": true}, "use_framework": false},
"load_transparency_trust_list": {"introducedVersion": "0.12.0", "default_results": {"allowed": false}, "use_framework": false},
"host_network": {"introducedVersion": "0.12.0", "default_results": {"allowed": false}, "use_framework": false},
}
7 changes: 7 additions & 0 deletions pkg/securitypolicy/framework.rego
Original file line number Diff line number Diff line change
Expand Up @@ -1207,6 +1207,12 @@ runtime_logging := {"allowed": true} {
allow_runtime_logging
}

default host_network := {"allowed": false}

host_network := {"allowed": true} {
allow_host_network
}

# Helpers to get data from the fragment that is currently being loaded. Since
# input.namespace is the package name the fragment loaded as,
# data[input.namespace] can be used to access the fragment. This is only valid
Expand Down Expand Up @@ -2899,6 +2905,7 @@ allow_dump_stacks := data.policy.allow_dump_stacks
allow_runtime_logging := data.policy.allow_runtime_logging
allow_environment_variable_dropping := data.policy.allow_environment_variable_dropping
allow_unencrypted_scratch := data.policy.allow_unencrypted_scratch
allow_host_network := data.policy.allow_host_network

# all flags not in the base set need to have default logic applied

Expand Down
1 change: 1 addition & 0 deletions pkg/securitypolicy/open_door.rego
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,4 @@ load_fragment := {"allowed": true}
scratch_mount := {"allowed": true}
scratch_unmount := {"allowed": true}
load_transparency_trust_list := {"allowed": true}
host_network := {"allowed": true}
7 changes: 7 additions & 0 deletions pkg/securitypolicy/opts.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,13 @@ func WithAllowRuntimeLogging(allow bool) PolicyConfigOpt {
}
}

func WithAllowHostNetwork(allow bool) PolicyConfigOpt {
return func(config *PolicyConfig) error {
config.AllowHostNetwork = allow
return nil
}
}

func WithExternalProcesses(processes []ExternalProcessConfig) PolicyConfigOpt {
return func(config *PolicyConfig) error {
config.ExternalProcesses = append(config.ExternalProcesses, processes...)
Expand Down
1 change: 1 addition & 0 deletions pkg/securitypolicy/policy.rego
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,5 @@ load_fragment := data.framework.load_fragment
scratch_mount := data.framework.scratch_mount
scratch_unmount := data.framework.scratch_unmount
load_transparency_trust_list := data.framework.load_transparency_trust_list
host_network := data.framework.host_network
reason := data.framework.reason
2 changes: 2 additions & 0 deletions pkg/securitypolicy/policy_with_platform_rules.rego
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ containers := [
allow_properties_access := true
allow_dump_stacks := false
allow_runtime_logging := false
allow_host_network := false
allow_environment_variable_dropping := true
allow_unencrypted_scratch := false
allow_capability_dropping := true
Expand All @@ -102,6 +103,7 @@ plan9_unmount := data.framework.plan9_unmount
get_properties := data.framework.get_properties
dump_stacks := data.framework.dump_stacks
runtime_logging := data.framework.runtime_logging
host_network := data.framework.host_network
load_fragment := data.framework.load_fragment
scratch_mount := data.framework.scratch_mount
scratch_unmount := data.framework.scratch_unmount
Expand Down
6 changes: 6 additions & 0 deletions pkg/securitypolicy/rego_utils_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2025,6 +2025,7 @@ func (constraints *generatedConstraints) toPolicy() *securityPolicyInternal {
AllowPropertiesAccess: constraints.allowGetProperties,
AllowDumpStacks: constraints.allowDumpStacks,
AllowRuntimeLogging: constraints.allowRuntimeLogging,
AllowHostNetwork: constraints.allowHostNetwork,
AllowEnvironmentVariableDropping: constraints.allowEnvironmentVariableDropping,
AllowUnencryptedScratch: constraints.allowUnencryptedScratch,
AllowCapabilityDropping: constraints.allowCapabilityDropping,
Expand Down Expand Up @@ -2284,6 +2285,7 @@ func generateConstraints(r *rand.Rand, maxContainers int32) *generatedConstraint
allowGetProperties: randBool(r),
allowDumpStacks: randBool(r),
allowRuntimeLogging: false,
allowHostNetwork: randBool(r),
allowEnvironmentVariableDropping: false,
allowUnencryptedScratch: randBool(r),
namespace: generateFragmentNamespace(testRand),
Expand Down Expand Up @@ -2948,6 +2950,7 @@ type generatedConstraints struct {
allowGetProperties bool
allowDumpStacks bool
allowRuntimeLogging bool
allowHostNetwork bool
allowEnvironmentVariableDropping bool
allowUnencryptedScratch bool
namespace string
Expand All @@ -2963,6 +2966,7 @@ type generatedWindowsConstraints struct {
allowGetProperties bool
allowDumpStacks bool
allowRuntimeLogging bool
allowHostNetwork bool
allowEnvironmentVariableDropping bool
allowUnencryptedScratch bool
namespace string
Expand All @@ -2979,6 +2983,7 @@ func (constraints *generatedWindowsConstraints) toPolicy() *securityPolicyWindow
AllowPropertiesAccess: constraints.allowGetProperties,
AllowDumpStacks: constraints.allowDumpStacks,
AllowRuntimeLogging: constraints.allowRuntimeLogging,
AllowHostNetwork: constraints.allowHostNetwork,
AllowEnvironmentVariableDropping: constraints.allowEnvironmentVariableDropping,
AllowUnencryptedScratch: constraints.allowUnencryptedScratch,
AllowCapabilityDropping: constraints.allowCapabilityDropping,
Expand Down Expand Up @@ -3023,6 +3028,7 @@ func generateWindowsConstraints(r *rand.Rand, maxContainers int32) *generatedWin
allowGetProperties: randBool(r),
allowDumpStacks: randBool(r),
allowRuntimeLogging: false,
allowHostNetwork: randBool(r),
allowEnvironmentVariableDropping: false,
allowUnencryptedScratch: false,
allowCapabilityDropping: false,
Expand Down
31 changes: 31 additions & 0 deletions pkg/securitypolicy/regopolicy_linux_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ func Test_MarshalRego_Policy(t *testing.T) {
p.allowGetProperties,
p.allowDumpStacks,
p.allowRuntimeLogging,
p.allowHostNetwork,
p.allowEnvironmentVariableDropping,
p.allowUnencryptedScratch,
p.allowCapabilityDropping,
Expand Down Expand Up @@ -4170,6 +4171,36 @@ func Test_EnforceRuntimeLogging_Not_Allowed(t *testing.T) {
}
}

func Test_EnforceHostNetwork_Allowed(t *testing.T) {
gc := generateConstraints(testRand, maxContainersInGeneratedConstraints)
gc.allowHostNetwork = true

tc, err := setupRegoPolicyOnlyTest(gc)
if err != nil {
t.Fatalf("unable to setup test: %v", err)
}

err = tc.policy.EnforceHostNetworkPolicy(gc.ctx)
if err != nil {
t.Fatalf("Policy enforcement unexpectedly was denied: %v", err)
}
}

func Test_EnforceHostNetwork_Not_Allowed(t *testing.T) {
gc := generateConstraints(testRand, maxContainersInGeneratedConstraints)
gc.allowHostNetwork = false

tc, err := setupRegoPolicyOnlyTest(gc)
if err != nil {
t.Fatalf("unable to setup test: %v", err)
}

err = tc.policy.EnforceHostNetworkPolicy(gc.ctx)
if err == nil {
t.Fatalf("Policy enforcement unexpectedly was allowed")
}
}

func Test_Rego_LoadFragment_Container(t *testing.T) {
f := func(p *generatedConstraints) bool {
tc, err := setupRegoFragmentTestConfigWithIncludes(p, []string{"containers"})
Expand Down
1 change: 1 addition & 0 deletions pkg/securitypolicy/securitypolicy.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ type PolicyConfig struct {
AllowPropertiesAccess bool `json:"allow_properties_access" toml:"allow_properties_access"`
AllowDumpStacks bool `json:"allow_dump_stacks" toml:"allow_dump_stacks"`
AllowRuntimeLogging bool `json:"allow_runtime_logging" toml:"allow_runtime_logging"`
AllowHostNetwork bool `json:"allow_hostnetwork" toml:"allow_hostnetwork"`
AllowEnvironmentVariableDropping bool `json:"allow_environment_variable_dropping" toml:"allow_environment_variable_dropping"`
// AllowUnencryptedScratch is a global policy configuration that allows
// all containers within a pod to be run without scratch encryption.
Expand Down
4 changes: 4 additions & 0 deletions pkg/securitypolicy/securitypolicy_internal.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ type securityPolicyInternal struct {
AllowPropertiesAccess bool
AllowDumpStacks bool
AllowRuntimeLogging bool
AllowHostNetwork bool
AllowEnvironmentVariableDropping bool
AllowUnencryptedScratch bool
AllowCapabilityDropping bool
Expand All @@ -29,6 +30,7 @@ type securityPolicyWindowsInternal struct {
AllowPropertiesAccess bool
AllowDumpStacks bool
AllowRuntimeLogging bool
AllowHostNetwork bool
AllowEnvironmentVariableDropping bool
AllowUnencryptedScratch bool
AllowCapabilityDropping bool
Expand Down Expand Up @@ -94,6 +96,7 @@ func newSecurityPolicyInternal(
allowPropertiesAccess bool,
allowDumpStacks bool,
allowRuntimeLogging bool,
allowHostNetwork bool,
allowDropEnvironmentVariables bool,
allowUnencryptedScratch bool,
allowDropCapabilities bool,
Expand All @@ -110,6 +113,7 @@ func newSecurityPolicyInternal(
AllowPropertiesAccess: allowPropertiesAccess,
AllowDumpStacks: allowDumpStacks,
AllowRuntimeLogging: allowRuntimeLogging,
AllowHostNetwork: allowHostNetwork,
AllowEnvironmentVariableDropping: allowDropEnvironmentVariables,
AllowUnencryptedScratch: allowUnencryptedScratch,
AllowCapabilityDropping: allowDropCapabilities,
Expand Down
Loading
Loading