Repository: lima-vm/lima
Pull request #2734 (2024-10-15 05:05:01)

networks.Validate() requires that socket_vmnet is owned by root
This restriction was weakened by https://github.com/lima-vm/lima/pull/1220 to only require the file and directories to be owned by the admin user, but that configuration is not secure.

If users are willing to run an insecure configuration, then they can always enable password-less sudo, which does not need a sudoers file at all.


Patch:
diff --git a/.yamllint b/.yamllint
index dae683c8..a7c8c3f9 100644
--- a/.yamllint
+++ b/.yamllint
@@ -2,10 +2,6 @@
 
 extends: default
 
-ignore: |
-  # this is a yaml template, needs to be executed
-  pkg/cidata/cloud-config.yaml
-
 rules:
   indentation:
     indent-sequences: false
diff --git a/cmd/limactl/factory-reset.go b/cmd/limactl/factory-reset.go
index c13a450f..9e98f501 100644
--- a/cmd/limactl/factory-reset.go
+++ b/cmd/limactl/factory-reset.go
@@ -6,7 +6,6 @@ import (
 	"path/filepath"
 	"strings"
 
-	"github.com/lima-vm/lima/pkg/cidata"
 	"github.com/lima-vm/lima/pkg/instance"
 	"github.com/lima-vm/lima/pkg/store"
 	"github.com/lima-vm/lima/pkg/store/filenames"
@@ -64,11 +63,6 @@ func factoryResetAction(_ *cobra.Command, args []string) error {
 			}
 		}
 	}
-	// Regenerate the cloud-config.yaml, to reflect any changes to the global _config
-	if err := cidata.GenerateCloudConfig(inst.Dir, instName, inst.Config); err != nil {
-		logrus.Error(err)
-	}
-
 	logrus.Infof("Instance %q has been factory reset", instName)
 	return nil
 }
diff --git a/cmd/limactl/start.go b/cmd/limactl/start.go
index e7311d5a..34f4f2d1 100644
--- a/cmd/limactl/start.go
+++ b/cmd/limactl/start.go
@@ -131,12 +131,6 @@ func loadOrCreateInstance(cmd *cobra.Command, args []string, createOnly bool) (*
 		// No need to use SecureJoin here. https://github.com/lima-vm/lima/pull/805#discussion_r853411702
 		templateName := filepath.Join(templateURL.Host, templateURL.Path)
 		logrus.Debugf("interpreting argument %q as a template name %q", arg, templateName)
-		switch templateName {
-		case "experimental/riscv64":
-			logrus.Warn("template://experimental/riscv64 was merged into the default template in Lima v1.0. Use `limactl create --arch=riscv64 template://default` instead.")
-		case "experimental/armv7l":
-			logrus.Warn("template://experimental/armv7l was merged into the default template in Lima v1.0. Use `limactl create --arch=armv7l template://default` instead.")
-		}
 		if st.instName == "" {
 			// e.g., templateName = "deprecated/centos-7" , st.instName = "centos-7"
 			st.instName = filepath.Base(templateName)
diff --git a/cmd/limactl/sudoers.go b/cmd/limactl/sudoers.go
index 9b54adfc..afe4c5ed 100644
--- a/cmd/limactl/sudoers.go
+++ b/cmd/limactl/sudoers.go
@@ -3,21 +3,16 @@ package main
 import (
 	"errors"
 	"fmt"
-	"os"
-	"path/filepath"
 	"runtime"
 
 	"github.com/lima-vm/lima/pkg/networks"
+	"github.com/sirupsen/logrus"
 	"github.com/spf13/cobra"
 )
 
+const networksURL = "https://lima-vm.io/docs/config/network/#socket_vmnet"
+
 func newSudoersCommand() *cobra.Command {
-	networksMD := "$PREFIX/share/doc/lima/docs/network.md"
-	if exe, err := os.Executable(); err == nil {
-		binDir := filepath.Dir(exe)
-		prefixDir := filepath.Dir(binDir)
-		networksMD = filepath.Join(prefixDir, "share/doc/lima/docs/network.md")
-	}
 	sudoersCommand := &cobra.Command{
 		Use: "sudoers [--check [SUDOERSFILE-TO-CHECK]]",
 		Example: `
@@ -31,7 +26,7 @@ $ limactl sudoers --check /etc/sudoers.d/lima
 		Long: fmt.Sprintf(`Generate the content of the /etc/sudoers.d/lima file for enabling vmnet.framework support.
 The content is written to stdout, NOT to the file.
 This command must not run as the root user.
-See %s for the usage.`, networksMD),
+See %s for the usage.`, networksURL),
 		Args:    WrapArgsError(cobra.MaximumNArgs(1)),
 		RunE:    sudoersAction,
 		GroupID: advancedCommand,
@@ -52,6 +47,7 @@ func sudoersAction(cmd *cobra.Command, args []string) error {
 	}
 	// Make sure the current network configuration is secure
 	if err := nwCfg.Validate(); err != nil {
+		logrus.Infof("Please check %s for more information.", networksURL)
 		return err
 	}
 	check, err := cmd.Flags().GetBool("check")
diff --git a/examples/README.md b/examples/README.md
index 9632b083..c25e48c1 100644
--- a/examples/README.md
+++ b/examples/README.md
@@ -52,6 +52,8 @@ Optional feature enablers:
 - [`vmnet`](./vmnet.yaml): ⭐enable [`vmnet.framework`](../docs/network.md)
 - [`experimental/9p`](./experimental/9p.yaml): [experimental] use 9p mount type
 - [`experimental/virtiofs-linux`](./experimental/9p.yaml): [experimental] use virtiofs mount type for Linux
+- [`experimental/armv7l`](./experimental/armv7l.yaml): [experimental] ARMv7
+- [`experimental/riscv64`](./experimental/riscv64.yaml): [experimental] RISC-V
 - [`experimental/net-user-v2`](./experimental/net-user-v2.yaml): [experimental] user-v2 network
   to enable VM-to-VM communication without root privilege
 - [`experimental/vnc`](./experimental/vnc.yaml): [experimental] use vnc display and xorg server
@@ -68,8 +70,6 @@ Lost+found:
 - ~`nomad`~: Removed in Lima v0.17.1, as Nomad is [no longer free software](https://github.com/hashicorp/nomad/commit/b3e30b1dfa185d9437a25830522da47b91f78816)
 - ~`centos-stream-8`~: Remove in Lima v0.23.0, as CentOS Stream 8 reached [EOL](https://blog.centos.org/2023/04/end-dates-are-coming-for-centos-stream-8-and-centos-linux-7/).
 - ~`deprecated/centos-7`~: Remove in Lima v0.23.0, as CentOS 7 reached [EOL](https://blog.centos.org/2023/04/end-dates-are-coming-for-centos-stream-8-and-centos-linux-7/).
-- ~`experimental/armv7l`~: Merged into the `default` template in Lima v1.0. Use `limactl create --arch=armv7l template://default`.
-- ~`experimental/riscv64`~: Merged into the `default` template in Lima v1.0. Use `limactl create --arch=riscv64 template://default`.
 
 ## Tier
 
diff --git a/examples/default.yaml b/examples/default.yaml
index a8697c93..5e112460 100644
--- a/examples/default.yaml
+++ b/examples/default.yaml
@@ -42,22 +42,12 @@ images:
 - location: "https://cloud-images.ubuntu.com/releases/24.04/release-20240821/ubuntu-24.04-server-cloudimg-arm64.img"
   arch: "aarch64"
   digest: "sha256:5ecac6447be66a164626744a87a27fd4e6c6606dc683e0a233870af63df4276a"
-- location: "https://cloud-images.ubuntu.com/releases/24.04/release-20240821/ubuntu-24.04-server-cloudimg-riscv64.img"
-  arch: "riscv64"
-  digest: "sha256:f5886ad4e405e689585dfef0e96c31b06478e0cb12bc7f3fae965759a32d729e"
-- location: "https://cloud-images.ubuntu.com/releases/24.04/release-20240821/ubuntu-24.04-server-cloudimg-armhf.img"
-  arch: "armv7l"
-  digest: "sha256:8d5756a9424a5a75f8640539f3119b3cfeb31341bbb01f4968f13df4427ce489"
 # Fallback to the latest release image.
 # Hint: run `limactl prune` to invalidate the cache
 - location: "https://cloud-images.ubuntu.com/releases/24.04/release/ubuntu-24.04-server-cloudimg-amd64.img"
   arch: "x86_64"
 - location: "https://cloud-images.ubuntu.com/releases/24.04/release/ubuntu-24.04-server-cloudimg-arm64.img"
   arch: "aarch64"
-- location: "https://cloud-images.ubuntu.com/releases/24.04/release/ubuntu-24.04-server-cloudimg-riscv64.img"
-  arch: "riscv64"
-- location: "https://cloud-images.ubuntu.com/releases/24.04/release/ubuntu-24.04-server-cloudimg-armhf.img"
-  arch: "armv7l"
 
 # CPUs
 # 🟢 Builtin default: min(4, host CPU cores)
@@ -210,7 +200,7 @@ containerd:
   # 🟢 Builtin default: false
   system: null
   # Enable user-scoped (aka rootless) containerd and its dependencies
-  # 🟢 Builtin default: true (for x86_64 and aarch64)
+  # 🟢 Builtin default: true
   user: null
 #  # Override containerd archive
 #  # 🟢 Builtin default: hard-coded URL with hard-coded digest (see the output of `limactl info | jq .defaultTemplate.containerd.archives`)
diff --git a/examples/experimental/armv7l.yaml b/examples/experimental/armv7l.yaml
new file mode 100644
index 00000000..09025449
--- /dev/null
+++ b/examples/experimental/armv7l.yaml
@@ -0,0 +1,23 @@
+# This template requires Lima v0.17.0 or later.
+
+arch: "armv7l"
+images:
+# Try to use release-yyyyMMdd image if available. Note that release-yyyyMMdd will be removed after several months.
+- location: "https://cloud-images.ubuntu.com/releases/24.04/release-20240821/ubuntu-24.04-server-cloudimg-armhf.img"
+  arch: "armv7l"
+  digest: "sha256:8d5756a9424a5a75f8640539f3119b3cfeb31341bbb01f4968f13df4427ce489"
+# Fallback to the latest release image.
+# Hint: run `limactl prune` to invalidate the cache
+- location: "https://cloud-images.ubuntu.com/releases/24.04/release/ubuntu-24.04-server-cloudimg-armhf.img"
+  arch: "armv7l"
+
+mounts:
+- location: "~"
+- location: "/tmp/lima"
+  writable: true
+mountType: "9p"
+
+# We do not have arm-v7 binaries of containerd
+containerd:
+  system: false
+  user: false
diff --git a/examples/experimental/riscv64.yaml b/examples/experimental/riscv64.yaml
new file mode 100644
index 00000000..97861379
--- /dev/null
+++ b/examples/experimental/riscv64.yaml
@@ -0,0 +1,26 @@
+# Lima v0.x users should use https://raw.githubusercontent.com/lima-vm/lima/v0.23.2/examples/experimental/riscv64.yaml
+minimumLimaVersion: "1.0.0"
+vmOpts:
+  qemu:
+    minimumVersion: "9.1.0"
+
+arch: "riscv64"
+images:
+- location: "https://cloud-images.ubuntu.com/releases/24.04/release-20240821/ubuntu-24.04-server-cloudimg-riscv64.img"
+  arch: "riscv64"
+  digest: "sha256:f5886ad4e405e689585dfef0e96c31b06478e0cb12bc7f3fae965759a32d729e"
+# Fallback to the latest release image.
+# Hint: run `limactl prune` to invalidate the cache
+- location: "https://cloud-images.ubuntu.com/releases/24.04/release/ubuntu-24.04-server-cloudimg-riscv64.img"
+  arch: "riscv64"
+
+mounts:
+- location: "~"
+- location: "/tmp/lima"
+  writable: true
+mountType: "9p"
+
+# We do not have riscv64 binaries of containerd
+containerd:
+  system: false
+  user: false
diff --git a/examples/ubuntu.yaml b/examples/ubuntu.yaml
index dfb16afd..a7fb0b32 100644
--- a/examples/ubuntu.yaml
+++ b/examples/ubuntu.yaml
@@ -7,22 +7,12 @@ images:
 - location: "https://cloud-images.ubuntu.com/releases/24.04/release-20240821/ubuntu-24.04-server-cloudimg-arm64.img"
   arch: "aarch64"
   digest: "sha256:5ecac6447be66a164626744a87a27fd4e6c6606dc683e0a233870af63df4276a"
-- location: "https://cloud-images.ubuntu.com/releases/24.04/release-20240821/ubuntu-24.04-server-cloudimg-riscv64.img"
-  arch: "riscv64"
-  digest: "sha256:f5886ad4e405e689585dfef0e96c31b06478e0cb12bc7f3fae965759a32d729e"
-- location: "https://cloud-images.ubuntu.com/releases/24.04/release-20240821/ubuntu-24.04-server-cloudimg-armhf.img"
-  arch: "armv7l"
-  digest: "sha256:8d5756a9424a5a75f8640539f3119b3cfeb31341bbb01f4968f13df4427ce489"
 # Fallback to the latest release image.
 # Hint: run `limactl prune` to invalidate the cache
 - location: "https://cloud-images.ubuntu.com/releases/24.04/release/ubuntu-24.04-server-cloudimg-amd64.img"
   arch: "x86_64"
 - location: "https://cloud-images.ubuntu.com/releases/24.04/release/ubuntu-24.04-server-cloudimg-arm64.img"
   arch: "aarch64"
-- location: "https://cloud-images.ubuntu.com/releases/24.04/release/ubuntu-24.04-server-cloudimg-riscv64.img"
-  arch: "riscv64"
-- location: "https://cloud-images.ubuntu.com/releases/24.04/release/ubuntu-24.04-server-cloudimg-armhf.img"
-  arch: "armv7l"
 
 mounts:
 - location: "~"
diff --git a/examples/vmnet.yaml b/examples/vmnet.yaml
index 0cdbc254..cd92752f 100644
--- a/examples/vmnet.yaml
+++ b/examples/vmnet.yaml
@@ -1,10 +1,6 @@
 # A template to enable vmnet.framework.
 
-# Usage:
-#   brew install socket_vmnet
-#   limactl sudoers >etc_sudoers.d_lima
-#   sudo install -o root etc_sudoers.d_lima /etc/sudoers.d/lima
-#   limactl start template://vmnet
+# Install socket_vmnet: https://lima-vm.io/docs/config/network/#socket_vmnet
 
 # This template requires Lima v0.7.0 or later.
 # Older versions of Lima were using a different syntax for supporting vmnet.framework.
diff --git a/go.mod b/go.mod
index 6c65fe02..accec9d3 100644
--- a/go.mod
+++ b/go.mod
@@ -10,7 +10,7 @@ require (
 	github.com/apparentlymart/go-cidr v1.1.0
 	github.com/balajiv113/fd v0.0.0-20230330094840-143eec500f3e
 	github.com/cheggaaa/pb/v3 v3.1.5
-	github.com/containerd/containerd v1.7.23
+	github.com/containerd/containerd v1.7.22
 	github.com/containerd/continuity v0.4.3
 	github.com/containers/gvisor-tap-vsock v0.7.5
 	github.com/coreos/go-semver v0.3.1
@@ -59,7 +59,7 @@ require (
 	github.com/alecthomas/participle/v2 v2.1.1 // indirect
 	github.com/bmatcuk/doublestar/v4 v4.6.0 // indirect
 	github.com/braydonk/yaml v0.7.0 // indirect
-	github.com/containerd/errdefs v0.3.0 // indirect
+	github.com/containerd/errdefs v0.1.0 // indirect
 	github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
 	github.com/digitalocean/go-libvirt v0.0.0-20220804181439-8648fbde413e // indirect
 	github.com/dimchansky/utfbom v1.1.1 // indirect
diff --git a/go.sum b/go.sum
index 711efbdb..23daa1c0 100644
--- a/go.sum
+++ b/go.sum
@@ -29,12 +29,12 @@ github.com/braydonk/yaml v0.7.0 h1:ySkqO7r0MGoCNhiRJqE0Xe9yhINMyvOAB3nFjgyJn2k=
 github.com/braydonk/yaml v0.7.0/go.mod h1:hcm3h581tudlirk8XEUPDBAimBPbmnL0Y45hCRl47N4=
 github.com/cheggaaa/pb/v3 v3.1.5 h1:QuuUzeM2WsAqG2gMqtzaWithDJv0i+i6UlnwSCI4QLk=
 github.com/cheggaaa/pb/v3 v3.1.5/go.mod h1:CrxkeghYTXi1lQBEI7jSn+3svI3cuc19haAj6jM60XI=
-github.com/containerd/containerd v1.7.23 h1:H2CClyUkmpKAGlhQp95g2WXHfLYc7whAuvZGBNYOOwQ=
-github.com/containerd/containerd v1.7.23/go.mod h1:7QUzfURqZWCZV7RLNEn1XjUCQLEf0bkaK4GjUaZehxw=
+github.com/containerd/containerd v1.7.22 h1:nZuNnNRA6T6jB975rx2RRNqqH2k6ELYKDZfqTHqwyy0=
+github.com/containerd/containerd v1.7.22/go.mod h1:e3Jz1rYRUZ2Lt51YrH9Rz0zPyJBOlSvB3ghr2jbVD8g=
 github.com/containerd/continuity v0.4.3 h1:6HVkalIp+2u1ZLH1J/pYX2oBVXlJZvh1X1A7bEZ9Su8=
 github.com/containerd/continuity v0.4.3/go.mod h1:F6PTNCKepoxEaXLQp3wDAjygEnImnZ/7o4JzpodfroQ=
-github.com/containerd/errdefs v0.3.0 h1:FSZgGOeK4yuT/+DnF07/Olde/q4KBoMsaamhXxIMDp4=
-github.com/containerd/errdefs v0.3.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
+github.com/containerd/errdefs v0.1.0 h1:m0wCRBiu1WJT/Fr+iOoQHMQS/eP5myQ8lCv4Dz5ZURM=
+github.com/containerd/errdefs v0.1.0/go.mod h1:YgWiiHtLmSeBrvpw+UfPijzbLaB77mEG1WwJTDETIV0=
 github.com/containers/gvisor-tap-vsock v0.7.5 h1:bTy4u3DOmmUPwurL6me2rsgfypAFDhyeJleUcQmBR/E=
 github.com/containers/gvisor-tap-vsock v0.7.5/go.mod h1:GW9jOqAEEGdaS20XwTYdm6KCYDHIulOE/yEEOabkoE4=
 github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4=
diff --git a/pkg/cidata/cidata.TEMPLATE.d/user-data b/pkg/cidata/cidata.TEMPLATE.d/user-data
index d98d0359..5bbd9940 100644
--- a/pkg/cidata/cidata.TEMPLATE.d/user-data
+++ b/pkg/cidata/cidata.TEMPLATE.d/user-data
@@ -39,7 +39,6 @@ users:
       - {{ printf "%q" $val }}
     {{- end }}
 
-{{- if .BootScripts }}
 write_files:
  - content: |
       #!/bin/sh
@@ -53,7 +52,6 @@ write_files:
    owner: root:root
    path: /var/lib/cloud/scripts/per-boot/00-lima.boot.sh
    permissions: '0755'
-{{- end }}
 
 {{- if .DNSAddresses }}
 # This has no effect on systems using systemd-resolved, but is used
@@ -68,12 +66,9 @@ resolv_conf:
   {{- end }}
 {{- end }}
 
-{{- if or .CACerts.RemoveDefaults .CACerts.Trusted }}
 {{ with .CACerts }}
 ca_certs:
-  {{- if .RemoveDefaults }}
   remove_defaults: {{ .RemoveDefaults }}
-  {{- end }}
   {{- if .Trusted}}
   trusted:
   {{- range $cert := .Trusted }}
@@ -81,7 +76,6 @@ ca_certs:
     {{- range $line := $cert.Lines }}
     {{ $line }}
     {{- end }}
-    {{- end }}
   {{- end }}
   {{- end }}
 {{- end }}
diff --git a/pkg/cidata/cidata.go b/pkg/cidata/cidata.go
index 722c1a6c..80d8d6ad 100644
--- a/pkg/cidata/cidata.go
+++ b/pkg/cidata/cidata.go
@@ -111,20 +111,19 @@ func setupEnv(instConfigEnv map[string]string, propagateProxyEnv bool, slirpGate
 	return env, nil
 }
 
-func templateArgs(bootScripts bool, instDir, name string, instConfig *limayaml.LimaYAML, udpDNSLocalPort, tcpDNSLocalPort, vsockPort int, virtioPort string) (*TemplateArgs, error) {
+func GenerateISO9660(instDir, name string, instConfig *limayaml.LimaYAML, udpDNSLocalPort, tcpDNSLocalPort int, nerdctlArchive string, vsockPort int, virtioPort string) error {
 	if err := limayaml.Validate(instConfig, false); err != nil {
-		return nil, err
+		return err
 	}
 	u, err := osutil.LimaUser(true)
 	if err != nil {
-		return nil, err
+		return err
 	}
 	uid, err := strconv.Atoi(u.Uid)
 	if err != nil {
-		return nil, err
+		return err
 	}
 	args := TemplateArgs{
-		BootScripts:        bootScripts,
 		Name:               name,
 		User:               u.Username,
 		UID:                uid,
@@ -151,14 +150,14 @@ func templateArgs(bootScripts bool, instDir, name string, instConfig *limayaml.L
 		usernetName := instConfig.Networks[firstUsernetIndex].Lima
 		subnet, err = usernet.Subnet(usernetName)
 		if err != nil {
-			return nil, err
+			return err
 		}
 		args.SlirpGateway = usernet.GatewayIP(subnet)
 		args.SlirpDNS = usernet.GatewayIP(subnet)
 	} else {
 		subnet, _, err = net.ParseCIDR(networks.SlirpNetwork)
 		if err != nil {
-			return nil, err
+			return err
 		}
 		args.SlirpGateway = usernet.GatewayIP(subnet)
 		if *instConfig.VMType == limayaml.VZ {
@@ -174,10 +173,10 @@ func templateArgs(bootScripts bool, instDir, name string, instConfig *limayaml.L
 
 	pubKeys, err := sshutil.DefaultPubKeys(*instConfig.SSH.LoadDotSSHPubKeys)
 	if err != nil {
-		return nil, err
+		return err
 	}
 	if len(pubKeys) == 0 {
-		return nil, errors.New("no SSH key was found, run `ssh-keygen`")
+		return errors.New("no SSH key was found, run `ssh-keygen`")
 	}
 	for _, f := range pubKeys {
 		args.SSHPubKeys = append(args.SSHPubKeys, f.Content)
@@ -194,17 +193,17 @@ func templateArgs(bootScripts bool, instDir, name string, instConfig *limayaml.L
 	}
 	hostHome, err := localpathutil.Expand("~")
 	if err != nil {
-		return nil, err
+		return err
 	}
 	for i, f := range instConfig.Mounts {
 		tag := fmt.Sprintf("mount%d", i)
 		location, err := localpathutil.Expand(f.Location)
 		if err != nil {
-			return nil, err
+			return err
 		}
 		mountPoint, err := localpathutil.Expand(f.MountPoint)
 		if err != nil {
-			return nil, err
+			return err
 		}
 		options := "defaults"
 		switch fstype {
@@ -218,7 +217,7 @@ func templateArgs(bootScripts bool, instDir, name string, instConfig *limayaml.L
 				options += fmt.Sprintf(",version=%s", *f.NineP.ProtocolVersion)
 				msize, err := units.RAMInBytes(*f.NineP.Msize)
 				if err != nil {
-					return nil, fmt.Errorf("failed to parse msize for %q: %w", location, err)
+					return fmt.Errorf("failed to parse msize for %q: %w", location, err)
 				}
 				options += fmt.Sprintf(",msize=%d", msize)
 				options += fmt.Sprintf(",cache=%s", *f.NineP.Cache)
@@ -269,7 +268,7 @@ func templateArgs(bootScripts bool, instDir, name string, instConfig *limayaml.L
 
 	args.Env, err = setupEnv(instConfig.Env, *instConfig.PropagateProxyEnv, args.SlirpGateway)
 	if err != nil {
-		return nil, err
+		return err
 	}
 
 	switch {
@@ -286,7 +285,7 @@ func templateArgs(bootScripts bool, instDir, name string, instConfig *limayaml.L
 	default:
 		args.DNSAddresses, err = osutil.DNSAddresses()
 		if err != nil {
-			return nil, err
+			return err
 		}
 	}
 
@@ -295,12 +294,12 @@ func templateArgs(bootScripts bool, instDir, name string, instConfig *limayaml.L
 	for _, path := range instConfig.CACertificates.Files {
 		expanded, err := localpathutil.Expand(path)
 		if err != nil {
-			return nil, err
+			return err
 		}
 
 		content, err := os.ReadFile(expanded)
 		if err != nil {
-			return nil, err
+			return err
 		}
 
 		cert := getCert(string(content))
@@ -312,12 +311,6 @@ func templateArgs(bootScripts bool, instDir, name string, instConfig *limayaml.L
 		args.CACerts.Trusted = append(args.CACerts.Trusted, cert)
 	}
 
-	// Remove empty caCerts (default values) from configuration yaml
-	if !*args.CACerts.RemoveDefaults && len(args.CACerts.Trusted) == 0 {
-		args.CACerts.RemoveDefaults = nil
-		args.CACerts.Trusted = nil
-	}
-
 	args.BootCmds = getBootCmds(instConfig.Provision)
 
 	for _, f := range instConfig.Provision {
@@ -326,43 +319,11 @@ func templateArgs(bootScripts bool, instDir, name string, instConfig *limayaml.L
 		}
 	}
 
-	return &args, nil
-}
-
-func GenerateCloudConfig(instDir, name string, instConfig *limayaml.LimaYAML) error {
-	args, err := templateArgs(false, instDir, name, instConfig, 0, 0, 0, "")
-	if err != nil {
-		return err
-	}
-	// mounts are not included here
-	args.Mounts = nil
-	// resolv_conf is not included here
-	args.DNSAddresses = nil
-
-	if err := ValidateTemplateArgs(args); err != nil {
-		return err
-	}
-
-	config, err := ExecuteTemplateCloudConfig(args)
-	if err != nil {
-		return err
-	}
-
-	os.RemoveAll(filepath.Join(instDir, filenames.CloudConfig)) // delete existing
-	return os.WriteFile(filepath.Join(instDir, filenames.CloudConfig), config, 0o444)
-}
-
-func GenerateISO9660(instDir, name string, instConfig *limayaml.LimaYAML, udpDNSLocalPort, tcpDNSLocalPort int, nerdctlArchive string, vsockPort int, virtioPort string) error {
-	args, err := templateArgs(true, instDir, name, instConfig, udpDNSLocalPort, tcpDNSLocalPort, vsockPort, virtioPort)
-	if err != nil {
-		return err
-	}
-
 	if err := ValidateTemplateArgs(args); err != nil {
 		return err
 	}
 
-	layout, err := ExecuteTemplateCIDataISO(args)
+	layout, err := ExecuteTemplate(args)
 	if err != nil {
 		return err
 	}
diff --git a/pkg/cidata/cloud-config.yaml b/pkg/cidata/cloud-config.yaml
deleted file mode 120000
index 4bec8830..00000000
--- a/pkg/cidata/cloud-config.yaml
+++ /dev/null
@@ -1,1 +0,0 @@
-cidata.TEMPLATE.d/user-data
\ No newline at end of file
diff --git a/pkg/cidata/template.go b/pkg/cidata/template.go
index 60abc16a..2a18ba43 100644
--- a/pkg/cidata/template.go
+++ b/pkg/cidata/template.go
@@ -75,7 +75,6 @@ type TemplateArgs struct {
 	TCPDNSLocalPort                 int
 	Env                             map[string]string
 	Param                           map[string]string
-	BootScripts                     bool
 	DNSAddresses                    []string
 	CACerts                         CACerts
 	HostHomeMountPoint              string
@@ -90,7 +89,7 @@ type TemplateArgs struct {
 	TimeZone                        string
 }
 
-func ValidateTemplateArgs(args *TemplateArgs) error {
+func ValidateTemplateArgs(args TemplateArgs) error {
 	if err := identifiers.Validate(args.Name); err != nil {
 		return err
 	}
@@ -115,24 +114,13 @@ func ValidateTemplateArgs(args *TemplateArgs) error {
 			return fmt.Errorf("field mounts[%d] must be absolute, got %q", i, f)
 		}
 	}
-	return nil
-}
-
-func ExecuteTemplateCloudConfig(args *TemplateArgs) ([]byte, error) {
-	if err := ValidateTemplateArgs(args); err != nil {
-		return nil, err
+	if args.CACerts.RemoveDefaults == nil {
+		return errors.New("field CACerts.RemoveDefaults must be set")
 	}
-
-	userData, err := templateFS.ReadFile(path.Join(templateFSRoot, "user-data"))
-	if err != nil {
-		return nil, err
-	}
-
-	cloudConfigYaml := string(userData)
-	return textutil.ExecuteTemplate(cloudConfigYaml, args)
+	return nil
 }
 
-func ExecuteTemplateCIDataISO(args *TemplateArgs) ([]iso9660util.Entry, error) {
+func ExecuteTemplate(args TemplateArgs) ([]iso9660util.Entry, error) {
 	if err := ValidateTemplateArgs(args); err != nil {
 		return nil, err
 	}
diff --git a/pkg/downloader/downloader.go b/pkg/downloader/downloader.go
index 26117401..c64495d7 100644
--- a/pkg/downloader/downloader.go
+++ b/pkg/downloader/downloader.go
@@ -12,15 +12,14 @@ import (
 	"os/exec"
 	"path"
 	"path/filepath"
+	"strconv"
 	"strings"
-	"sync/atomic"
 	"time"
 
 	"github.com/cheggaaa/pb/v3"
 	"github.com/containerd/continuity/fs"
 	"github.com/lima-vm/lima/pkg/httpclientutil"
 	"github.com/lima-vm/lima/pkg/localpathutil"
-	"github.com/lima-vm/lima/pkg/lockutil"
 	"github.com/lima-vm/lima/pkg/progressbar"
 	"github.com/opencontainers/go-digest"
 	"github.com/sirupsen/logrus"
@@ -268,21 +267,21 @@ func Download(ctx context.Context, local, remote string, opts ...Opt) (*Result,
 		return nil, err
 	}
 	shadURL := filepath.Join(shad, "url")
-	if err := writeFirst(shadURL, []byte(remote), 0o644); err != nil {
+	if err := atomicWrite(shadURL, []byte(remote), 0o644); err != nil {
 		return nil, err
 	}
 	if err := downloadHTTP(ctx, shadData, shadTime, shadType, remote, o.description, o.expectedDigest); err != nil {
 		return nil, err
 	}
-	if shadDigest != "" && o.expectedDigest != "" {
-		if err := writeFirst(shadDigest, []byte(o.expectedDigest.String()), 0o644); err != nil {
-			return nil, err
-		}
-	}
 	// no need to pass the digest to copyLocal(), as we already verified the digest
 	if err := copyLocal(ctx, localPath, shadData, ext, o.decompress, "", ""); err != nil {
 		return nil, err
 	}
+	if shadDigest != "" && o.expectedDigest != "" {
+		if err := atomicWrite(shadDigest, []byte(o.expectedDigest.String()), 0o644); err != nil {
+			return nil, err
+		}
+	}
 	res := &Result{
 		Status:          StatusDownloaded,
 		CachePath:       shadData,
@@ -606,13 +605,13 @@ func downloadHTTP(ctx context.Context, localPath, lastModified, contentType, url
 	}
 	if lastModified != "" {
 		lm := resp.Header.Get("Last-Modified")
-		if err := writeFirst(lastModified, []byte(lm), 0o644); err != nil {
+		if err := atomicWrite(lastModified, []byte(lm), 0o644); err != nil {
 			return err
 		}
 	}
 	if contentType != "" {
 		ct := resp.Header.Get("Content-Type")
-		if err := writeFirst(contentType, []byte(ct), 0o644); err != nil {
+		if err := atomicWrite(contentType, []byte(ct), 0o644); err != nil {
 			return err
 		}
 	}
@@ -673,43 +672,43 @@ func downloadHTTP(ctx context.Context, localPath, lastModified, contentType, url
 		return err
 	}
 
-	// If localPath was created by a parallel download keep it. Replacing it
-	// while another process is copying it to the destination may fail the
-	// clonefile syscall. We use a lock to ensure that only one process updates
-	// data, and when we return data file exists.
-
-	return lockutil.WithDirLock(filepath.Dir(localPath), func() error {
-		if _, err := os.Stat(localPath); err == nil {
-			return nil
-		} else if !errors.Is(err, os.ErrNotExist) {
-			return err
-		}
-		return os.Rename(localPathTmp, localPath)
-	})
+	return os.Rename(localPathTmp, localPath)
 }
 
-var tempfileCount atomic.Uint64
-
 // To allow parallel download we use a per-process unique suffix for tempoary
 // files. Renaming the temporary file to the final file is safe without
 // synchronization on posix.
-// To make it easy to test we also include a counter ensuring that each
-// temporary file is unique in the same process.
 // https://github.com/lima-vm/lima/issues/2722
 func perProcessTempfile(path string) string {
-	return fmt.Sprintf("%s.tmp.%d.%d", path, os.Getpid(), tempfileCount.Add(1))
+	return path + ".tmp." + strconv.FormatInt(int64(os.Getpid()), 10)
 }
 
-// writeFirst writes data to path unless path already exists.
-func writeFirst(path string, data []byte, perm os.FileMode) error {
-	return lockutil.WithDirLock(filepath.Dir(path), func() error {
-		if _, err := os.Stat(path); err == nil {
-			return nil
-		} else if !errors.Is(err, os.ErrNotExist) {
-			return err
+// atomicWrite writes data to path, creating a new file or replacing existing
+// one. Multiple processess can write to the same path safely. Safe on posix and
+// likely safe on windows when using NTFS.
+func atomicWrite(path string, data []byte, perm os.FileMode) error {
+	tmpPath := perProcessTempfile(path)
+	tmp, err := os.OpenFile(tmpPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, perm)
+	if err != nil {
+		return err
+	}
+	defer func() {
+		if err != nil {
+			tmp.Close()
+			os.RemoveAll(tmpPath)
 		}
-		return os.WriteFile(path, data, perm)
-	})
+	}()
+	if _, err = tmp.Write(data); err != nil {
+		return err
+	}
+	if err = tmp.Sync(); err != nil {
+		return err
+	}
+	if err = tmp.Close(); err != nil {
+		return err
+	}
+	err = os.Rename(tmpPath, path)
+	return err
 }
 
 // CacheEntries returns a map of cache entries.
diff --git a/pkg/hostagent/hostagent.go b/pkg/hostagent/hostagent.go
index 0c260b13..1125cfb9 100644
--- a/pkg/hostagent/hostagent.go
+++ b/pkg/hostagent/hostagent.go
@@ -133,9 +133,6 @@ func New(instName string, stdout io.Writer, signalCh chan os.Signal, opts ...Opt
 		virtioPort = "" // filenames.VirtioPort
 	}
 
-	if err := cidata.GenerateCloudConfig(inst.Dir, instName, inst.Config); err != nil {
-		return nil, err
-	}
 	if err := cidata.GenerateISO9660(inst.Dir, instName, inst.Config, udpDNSLocalPort, tcpDNSLocalPort, o.nerdctlArchive, vSockPort, virtioPort); err != nil {
 		return nil, err
 	}
diff --git a/pkg/instance/create.go b/pkg/instance/create.go
index 6a3f91d1..f8678cb4 100644
--- a/pkg/instance/create.go
+++ b/pkg/instance/create.go
@@ -7,7 +7,6 @@ import (
 	"os"
 	"path/filepath"
 
-	"github.com/lima-vm/lima/pkg/cidata"
 	"github.com/lima-vm/lima/pkg/driver"
 	"github.com/lima-vm/lima/pkg/driverutil"
 	"github.com/lima-vm/lima/pkg/limayaml"
@@ -61,9 +60,6 @@ func Create(ctx context.Context, instName string, instConfig []byte, saveBrokenY
 	if err := os.WriteFile(filePath, instConfig, 0o644); err != nil {
 		return nil, err
 	}
-	if err := cidata.GenerateCloudConfig(instDir, instName, loadedInstConfig); err != nil {
-		return nil, err
-	}
 	if err := os.WriteFile(filepath.Join(instDir, filenames.LimaVersion), []byte(version.Version), 0o444); err != nil {
 		return nil, err
 	}
diff --git a/pkg/limayaml/defaults.go b/pkg/limayaml/defaults.go
index 32817cee..0e48c65b 100644
--- a/pkg/limayaml/defaults.go
+++ b/pkg/limayaml/defaults.go
@@ -448,12 +448,7 @@ func FillDefault(y, d, o *LimaYAML, filePath string) {
 		y.Containerd.User = o.Containerd.User
 	}
 	if y.Containerd.User == nil {
-		switch *y.Arch {
-		case X8664, AARCH64:
-			y.Containerd.User = ptr.Of(true)
-		default:
-			y.Containerd.User = ptr.Of(false)
-		}
+		y.Containerd.User = ptr.Of(true)
 	}
 
 	y.Containerd.Archives = append(append(o.Containerd.Archives, y.Containerd.Archives...), d.Containerd.Archives...)
diff --git a/pkg/nativeimgutil/nativeimgutil.go b/pkg/nativeimgutil/nativeimgutil.go
index 1c314e6b..7db33c16 100644
--- a/pkg/nativeimgutil/nativeimgutil.go
+++ b/pkg/nativeimgutil/nativeimgutil.go
@@ -14,6 +14,7 @@ import (
 	"github.com/lima-vm/go-qcow2reader"
 	"github.com/lima-vm/go-qcow2reader/image/qcow2"
 	"github.com/lima-vm/go-qcow2reader/image/raw"
+	"github.com/lima-vm/lima/pkg/osutil"
 	"github.com/lima-vm/lima/pkg/progressbar"
 	"github.com/sirupsen/logrus"
 )
@@ -67,13 +68,6 @@ func ConvertToRaw(source, dest string, size *int64, allowSourceWithBackingFile b
 	defer os.RemoveAll(destTmp)
 	defer destTmpF.Close()
 
-	// Truncating before copy eliminates the seeks during copy and provide a
-	// hint to the file system that may minimize allocations and fragmanation
-	// of the file.
-	if err := MakeSparse(destTmpF, srcImg.Size()); err != nil {
-		return err
-	}
-
 	// Copy
 	srcImgR := io.NewSectionReader(srcImg, 0, srcImg.Size())
 	bar, err := progressbar.New(srcImg.Size())
@@ -130,8 +124,8 @@ func convertRawToRaw(source, dest string, size *int64) error {
 
 func copySparse(w *os.File, r io.Reader, bufSize int64) (int64, error) {
 	var (
-		n   int64
-		eof bool
+		n              int64
+		eof, hasWrites bool
 	)
 
 	zeroBuf := make([]byte, bufSize)
@@ -145,15 +139,20 @@ func copySparse(w *os.File, r io.Reader, bufSize int64) (int64, error) {
 			}
 		}
 		// TODO: qcow2reader should have a method to notify whether buf is zero
-		if bytes.Equal(buf[:rN], zeroBuf[:rN]) {
+		if bytes.Equal(buf, zeroBuf) {
+			if _, sErr := w.Seek(int64(rN), io.SeekCurrent); sErr != nil {
+				return n, fmt.Errorf("failed seek: %w", sErr)
+			}
+			// no need to ftruncate here
 			n += int64(rN)
 		} else {
-			wN, wErr := w.WriteAt(buf[:rN], n)
+			hasWrites = true
+			wN, wErr := w.Write(buf)
 			if wN > 0 {
 				n += int64(wN)
 			}
 			if wErr != nil {
-				return n, fmt.Errorf("failed to write: %w", wErr)
+				return n, fmt.Errorf("failed to read: %w", wErr)
 			}
 			if wN != rN {
 				return n, fmt.Errorf("read %d, but wrote %d bytes", rN, wN)
@@ -161,6 +160,10 @@ func copySparse(w *os.File, r io.Reader, bufSize int64) (int64, error) {
 		}
 	}
 
+	// Ftruncate must be run if the file contains only zeros
+	if !hasWrites {
+		return n, MakeSparse(w, n)
+	}
 	return n, nil
 }
 
@@ -168,5 +171,5 @@ func MakeSparse(f *os.File, n int64) error {
 	if _, err := f.Seek(n, io.SeekStart); err != nil {
 		return err
 	}
-	return f.Truncate(n)
+	return osutil.Ftruncate(int(f.Fd()), n)
 }
diff --git a/pkg/networks/networks.TEMPLATE.yaml b/pkg/networks/networks.TEMPLATE.yaml
index 88400878..b98e3db7 100644
--- a/pkg/networks/networks.TEMPLATE.yaml
+++ b/pkg/networks/networks.TEMPLATE.yaml
@@ -1,6 +1,6 @@
 # Path to socket_vmnet executable. Because socket_vmnet is invoked via sudo it should be
 # installed where only root can modify/replace it. This means also none of the
-# parent directories should be writable by the user.
+# parent directories can be writable by the user.
 #
 # The varRun directory also must not be writable by the user because it will
 # include the socket_vmnet pid file. Those will be terminated via sudo, so replacing
diff --git a/pkg/networks/validate.go b/pkg/networks/validate.go
index 6c2c1a49..37eb3c7d 100644
--- a/pkg/networks/validate.go
+++ b/pkg/networks/validate.go
@@ -5,11 +5,9 @@ import (
 	"fmt"
 	"io/fs"
 	"os"
-	"os/user"
 	"path/filepath"
 	"reflect"
 	"runtime"
-	"strconv"
 	"strings"
 
 	"github.com/lima-vm/lima/pkg/osutil"
@@ -100,49 +98,24 @@ func validatePath(path string, allowDaemonGroupWritable bool) error {
 	if err != nil {
 		return err
 	}
-	adminGroup, err := user.LookupGroup("admin")
-	if err != nil {
-		return err
-	}
-	adminGid, err := strconv.Atoi(adminGroup.Gid)
-	if err != nil {
-		return err
-	}
-	owner, err := user.LookupId(strconv.Itoa(int(stat.Uid)))
-	if err != nil {
-		return err
-	}
-	ownerIsAdmin := owner.Uid == "0"
-	if !ownerIsAdmin {
-		ownerGroupIDs, err := owner.GroupIds()
-		if err != nil {
-			return err
-		}
-		for _, g := range ownerGroupIDs {
-			if g == adminGroup.Gid {
-				ownerIsAdmin = true
-				break
-			}
-		}
-	}
-	if !ownerIsAdmin {
-		return fmt.Errorf(`%s %q owner %dis not an admin`, file, path, stat.Uid)
+	if stat.Uid != root.Uid {
+		return fmt.Errorf(`%s %q is not owned by %q (uid: %d), but by uid %d`, file, path, root.User, root.Uid, stat.Uid)
 	}
 	if allowDaemonGroupWritable {
 		daemon, err := osutil.LookupUser("daemon")
 		if err != nil {
 			return err
 		}
-		if fi.Mode()&0o20 != 0 && stat.Gid != root.Gid && stat.Gid != uint32(adminGid) && stat.Gid != daemon.Gid {
-			return fmt.Errorf(`%s %q is group-writable and group %d is not one of [wheel, admin, daemon]`,
-				file, path, stat.Gid)
+		if fi.Mode()&0o20 != 0 && stat.Gid != root.Gid && stat.Gid != daemon.Gid {
+			return fmt.Errorf(`%s %q is group-writable and group is neither %q (gid: %d) nor %q (gid: %d), but is gid: %d`,
+				file, path, root.User, root.Gid, daemon.User, daemon.Gid, stat.Gid)
 		}
 		if fi.Mode().IsDir() && fi.Mode()&1 == 0 && (fi.Mode()&0o010 == 0 || stat.Gid != daemon.Gid) {
 			return fmt.Errorf(`%s %q is not executable by the %q (gid: %d)" group`, file, path, daemon.User, daemon.Gid)
 		}
-	} else if fi.Mode()&0o20 != 0 && stat.Gid != root.Gid && stat.Gid != uint32(adminGid) {
-		return fmt.Errorf(`%s %q is group-writable and group %d is not one of [wheel, admin]`,
-			file, path, stat.Gid)
+	} else if fi.Mode()&0o20 != 0 && stat.Gid != root.Gid {
+		return fmt.Errorf(`%s %q is group-writable and group is not %q (gid: %d), but is gid: %d`,
+			file, path, root.User, root.Gid, stat.Gid)
 	}
 	if fi.Mode()&0o02 != 0 {
 		return fmt.Errorf("%s %q is world-writable", file, path)
diff --git a/pkg/osutil/osutil_unix.go b/pkg/osutil/osutil_unix.go
index 2420a8ef..7c83bd5a 100644
--- a/pkg/osutil/osutil_unix.go
+++ b/pkg/osutil/osutil_unix.go
@@ -13,6 +13,10 @@ func Dup2(oldfd, newfd int) (err error) {
 	return unix.Dup2(oldfd, newfd)
 }
 
+func Ftruncate(fd int, length int64) (err error) {
+	return unix.Ftruncate(fd, length)
+}
+
 func SignalName(sig os.Signal) string {
 	return unix.SignalName(sig.(syscall.Signal))
 }
diff --git a/pkg/osutil/osutil_windows.go b/pkg/osutil/osutil_windows.go
index c2f18bdd..39d1258b 100644
--- a/pkg/osutil/osutil_windows.go
+++ b/pkg/osutil/osutil_windows.go
@@ -38,6 +38,10 @@ func Dup2(oldfd int, newfd syscall.Handle) (err error) {
 	return fmt.Errorf("unimplemented")
 }
 
+func Ftruncate(_ int, _ int64) (err error) {
+	return fmt.Errorf("unimplemented")
+}
+
 func SignalName(sig os.Signal) string {
 	switch sig {
 	case syscall.SIGINT:
diff --git a/pkg/store/filenames/filenames.go b/pkg/store/filenames/filenames.go
index 18e589b0..0890156d 100644
--- a/pkg/store/filenames/filenames.go
+++ b/pkg/store/filenames/filenames.go
@@ -1,7 +1,7 @@
 // Package filenames defines the names of the files that appear under an instance dir
 // or inside the config directory.
 //
-// See docs/internal.md .
+// See https://lima-vm.io/docs/dev/internals/
 package filenames
 
 // Instance names starting with an underscore are reserved for lima internal usage
@@ -30,7 +30,6 @@ const (
 	LimaVersion          = "lima-version" // Lima version used to create instance
 	CIDataISO            = "cidata.iso"
 	CIDataISODir         = "cidata"
-	CloudConfig          = "cloud-config.yaml"
 	BaseDisk             = "basedisk"
 	DiffDisk             = "diffdisk"
 	Kernel               = "kernel"
diff --git a/website/content/en/docs/config/network/_index.md b/website/content/en/docs/config/network/_index.md
index 48150519..1eafc4b7 100644
--- a/website/content/en/docs/config/network/_index.md
+++ b/website/content/en/docs/config/network/_index.md
@@ -69,20 +69,38 @@ The configuration steps are different for each network type:
 #### Managed (192.168.105.0/24)
 
 [`socket_vmnet`](https://github.com/lima-vm/socket_vmnet) is required for adding another guest IP that is accessible from the host and other guests.
+It must be installed according to the instruction provided on https://github.com/lima-vm/socket_vmnet.
+
+Note that installation using Homebrew is not secure and not recommended by the Lima project.
+Homebrew installation will only work with Lima if password-less `sudo` is enabled for the current user.
+The `limactl sudoers` command requires that `socket_vmnet` is installed into a secure path only
+writable by `root` and will reject `socket_vmnet` installed by Homebrew into a user-writable location.
 
 ```bash
-# Install socket_vmnet
-brew install socket_vmnet
+# Install socket_vmnet as root from source to /opt/socket_vmnet
+# using instructions on https://github.com/lima-vm/socket_vmnet
+# This assumes that Xcode Command Line Tools are already installed
+git clone https://github.com/lima-vm/socket_vmnet
+cd socket_vmnet
+# Change "v1.1.5" to the actual latest release in https://github.com/lima-vm/socket_vmnet/releases
+git checkout v1.1.5
+make
+sudo make PREFIX=/opt/socket_vmnet install.bin
 
 # Set up the sudoers file for launching socket_vmnet from Lima
 limactl sudoers >etc_sudoers.d_lima
+less etc_sudoers.d_lima  # verify that the file looks correct
 sudo install -o root etc_sudoers.d_lima /etc/sudoers.d/lima
+rm etc_sudoers.d_lima
 ```
 
 > **Note**
 >
 > Lima before v0.12 used `vde_vmnet` for managing the networks.
 > `vde_vmnet` is no longer supported.
+>
+> Lima v0.14.0 and later used to also accept `socket_vmnet` installations if they were
+> owned by the `admin` user. Starting with v1.0.0 only `root` ownership is acceptable.
 
 The networks are defined in `$LIMA_HOME/_config/networks.yaml`. If this file doesn't already exist, it will be created with these default
 settings:
@@ -145,7 +163,7 @@ limactl start --network=lima:shared
 ```yaml
 networks:
   # Lima can manage the socket_vmnet daemon for networks defined in $LIMA_HOME/_config/networks.yaml automatically.
-  # The socket_vmnet binary must be installed into a secure location only alterable by the admin.
+  # The socket_vmnet binary must be installed into a secure location only alterable by the "root" user.
   # - lima: shared
   #   # MAC address of the instance; lima will pick one based on the instance name,
   #   # so DHCP assigned ip addresses should remain constant over instance restarts.
@@ -160,6 +178,17 @@ The network daemon is started automatically when the first instance referencing
 and will stop automatically once the last instance has stopped. Daemon logs will be stored in the
 `$LIMA_HOME/_networks` directory.
 
+Since the commands to start and stop the `socket_vmnet` daemon requires root, the user either must
+have password-less `sudo` enabled, or add the required commands to a `sudoers` file. This can
+be done via:
+
+```shell
+limactl sudoers >etc_sudoers.d_lima
+less etc_sudoers.d_lima  # verify that the file looks correct
+sudo install -o root etc_sudoers.d_lima /etc/sudoers.d/lima
+rm etc_sudoers.d_lima
+```
+
 The IP address is automatically assigned by macOS's bootpd.
 If the IP address is not assigned, try the following commands:
 ```bash
diff --git a/website/content/en/docs/dev/internals/_index.md b/website/content/en/docs/dev/internals/_index.md
index b71ce546..03d94eaf 100644
--- a/website/content/en/docs/dev/internals/_index.md
+++ b/website/content/en/docs/dev/internals/_index.md
@@ -37,7 +37,6 @@ Metadata:
 - `protected`: empty file, used by `limactl protect`
 
 cloud-init:
-- `cloud-config.yaml`: cloud-init configuration, for reference only.
 - `cidata.iso`: cloud-init ISO9660 image. See [`cidata.iso`](#cidataiso).
 
 Ansible:


Test patch:
diff --git a/pkg/cidata/template_test.go b/pkg/cidata/template_test.go
index 35c3623e..a8ae1cc8 100644
--- a/pkg/cidata/template_test.go
+++ b/pkg/cidata/template_test.go
@@ -10,45 +10,8 @@ import (
 
 var defaultRemoveDefaults = false
 
-func TestConfig(t *testing.T) {
-	args := &TemplateArgs{
-		Name: "default",
-		User: "foo",
-		UID:  501,
-		Home: "/home/foo.linux",
-		SSHPubKeys: []string{
-			"ssh-rsa dummy foo@example.com",
-		},
-		MountType: "reverse-sshfs",
-	}
-	config, err := ExecuteTemplateCloudConfig(args)
-	assert.NilError(t, err)
-	t.Log(string(config))
-	assert.Assert(t, !strings.Contains(string(config), "ca_certs:"))
-}
-
-func TestConfigCACerts(t *testing.T) {
-	args := &TemplateArgs{
-		Name: "default",
-		User: "foo",
-		UID:  501,
-		Home: "/home/foo.linux",
-		SSHPubKeys: []string{
-			"ssh-rsa dummy foo@example.com",
-		},
-		MountType: "reverse-sshfs",
-		CACerts: CACerts{
-			RemoveDefaults: &defaultRemoveDefaults,
-		},
-	}
-	config, err := ExecuteTemplateCloudConfig(args)
-	assert.NilError(t, err)
-	t.Log(string(config))
-	assert.Assert(t, strings.Contains(string(config), "ca_certs:"))
-}
-
 func TestTemplate(t *testing.T) {
-	args := &TemplateArgs{
+	args := TemplateArgs{
 		Name: "default",
 		User: "foo",
 		UID:  501,
@@ -66,7 +29,7 @@ func TestTemplate(t *testing.T) {
 			Trusted:        []Cert{},
 		},
 	}
-	layout, err := ExecuteTemplateCIDataISO(args)
+	layout, err := ExecuteTemplate(args)
 	assert.NilError(t, err)
 	for _, f := range layout {
 		t.Logf("=== %q ===", f.Path)
@@ -83,7 +46,7 @@ func TestTemplate(t *testing.T) {
 }
 
 func TestTemplate9p(t *testing.T) {
-	args := &TemplateArgs{
+	args := TemplateArgs{
 		Name: "default",
 		User: "foo",
 		UID:  501,
@@ -100,7 +63,7 @@ func TestTemplate9p(t *testing.T) {
 			RemoveDefaults: &defaultRemoveDefaults,
 		},
 	}
-	layout, err := ExecuteTemplateCIDataISO(args)
+	layout, err := ExecuteTemplate(args)
 	assert.NilError(t, err)
 	for _, f := range layout {
 		t.Logf("=== %q ===", f.Path)
diff --git a/pkg/downloader/downloader_test.go b/pkg/downloader/downloader_test.go
index 00c42c6f..136de343 100644
--- a/pkg/downloader/downloader_test.go
+++ b/pkg/downloader/downloader_test.go
@@ -8,7 +8,6 @@ import (
 	"os/exec"
 	"path/filepath"
 	"runtime"
-	"slices"
 	"strings"
 	"testing"
 	"time"
@@ -22,20 +21,6 @@ func TestMain(m *testing.M) {
 	os.Exit(m.Run())
 }
 
-type downloadResult struct {
-	r   *Result
-	err error
-}
-
-// We expect only few parallel downloads. Testing with larger number to find
-// races quicker. 20 parallel downloads take about 120 milliseocnds on M1 Pro.
-const parallelDownloads = 20
-
-// When downloading in parallel usually all downloads completed with
-// StatusDownload, but some may be delayed and find the data file when they
-// start. Can be reproduced locally using 100 parallel downloads.
-var parallelStatus = []Status{StatusDownloaded, StatusUsedCache}
-
 func TestDownloadRemote(t *testing.T) {
 	ts := httptest.NewServer(http.FileServer(http.Dir("testdata")))
 	t.Cleanup(ts.Close)
@@ -72,90 +57,38 @@ func TestDownloadRemote(t *testing.T) {
 		})
 	})
 	t.Run("with cache", func(t *testing.T) {
-		t.Run("serial", func(t *testing.T) {
-			cacheDir := filepath.Join(t.TempDir(), "cache")
-			localPath := filepath.Join(t.TempDir(), t.Name())
-			r, err := Download(context.Background(), localPath, dummyRemoteFileURL,
-				WithExpectedDigest(dummyRemoteFileDigest), WithCacheDir(cacheDir))
-			assert.NilError(t, err)
-			assert.Equal(t, StatusDownloaded, r.Status)
+		cacheDir := filepath.Join(t.TempDir(), "cache")
+		localPath := filepath.Join(t.TempDir(), t.Name())
+		r, err := Download(context.Background(), localPath, dummyRemoteFileURL, WithExpectedDigest(dummyRemoteFileDigest), WithCacheDir(cacheDir))
+		assert.NilError(t, err)
+		assert.Equal(t, StatusDownloaded, r.Status)
 
-			r, err = Download(context.Background(), localPath, dummyRemoteFileURL,
-				WithExpectedDigest(dummyRemoteFileDigest), WithCacheDir(cacheDir))
-			assert.NilError(t, err)
-			assert.Equal(t, StatusSkipped, r.Status)
+		r, err = Download(context.Background(), localPath, dummyRemoteFileURL, WithExpectedDigest(dummyRemoteFileDigest), WithCacheDir(cacheDir))
+		assert.NilError(t, err)
+		assert.Equal(t, StatusSkipped, r.Status)
 
-			localPath2 := localPath + "-2"
-			r, err = Download(context.Background(), localPath2, dummyRemoteFileURL,
-				WithExpectedDigest(dummyRemoteFileDigest), WithCacheDir(cacheDir))
-			assert.NilError(t, err)
-			assert.Equal(t, StatusUsedCache, r.Status)
-		})
-		t.Run("parallel", func(t *testing.T) {
-			cacheDir := filepath.Join(t.TempDir(), "cache")
-			results := make(chan downloadResult, parallelDownloads)
-			for i := 0; i < parallelDownloads; i++ {
-				go func() {
-					// Parallel download is supported only for different instances with unique localPath.
-					localPath := filepath.Join(t.TempDir(), t.Name())
-					r, err := Download(context.Background(), localPath, dummyRemoteFileURL,
-						WithExpectedDigest(dummyRemoteFileDigest), WithCacheDir(cacheDir))
-					results <- downloadResult{r, err}
-				}()
-			}
-			// We must process all results before cleanup.
-			for i := 0; i < parallelDownloads; i++ {
-				result := <-results
-				if result.err != nil {
-					t.Errorf("Download failed: %s", result.err)
-				} else if !slices.Contains(parallelStatus, result.r.Status) {
-					t.Errorf("Expected download status %s, got %s", parallelStatus, result.r.Status)
-				}
-			}
-		})
+		localPath2 := localPath + "-2"
+		r, err = Download(context.Background(), localPath2, dummyRemoteFileURL, WithExpectedDigest(dummyRemoteFileDigest), WithCacheDir(cacheDir))
+		assert.NilError(t, err)
+		assert.Equal(t, StatusUsedCache, r.Status)
 	})
 	t.Run("caching-only mode", func(t *testing.T) {
-		t.Run("serial", func(t *testing.T) {
-			_, err := Download(context.Background(), "", dummyRemoteFileURL, WithExpectedDigest(dummyRemoteFileDigest))
-			assert.ErrorContains(t, err, "cache directory to be specified")
+		_, err := Download(context.Background(), "", dummyRemoteFileURL, WithExpectedDigest(dummyRemoteFileDigest))
+		assert.ErrorContains(t, err, "cache directory to be specified")
 
-			cacheDir := filepath.Join(t.TempDir(), "cache")
-			r, err := Download(context.Background(), "", dummyRemoteFileURL, WithExpectedDigest(dummyRemoteFileDigest),
-				WithCacheDir(cacheDir))
-			assert.NilError(t, err)
-			assert.Equal(t, StatusDownloaded, r.Status)
+		cacheDir := filepath.Join(t.TempDir(), "cache")
+		r, err := Download(context.Background(), "", dummyRemoteFileURL, WithExpectedDigest(dummyRemoteFileDigest), WithCacheDir(cacheDir))
+		assert.NilError(t, err)
+		assert.Equal(t, StatusDownloaded, r.Status)
 
-			r, err = Download(context.Background(), "", dummyRemoteFileURL, WithExpectedDigest(dummyRemoteFileDigest),
-				WithCacheDir(cacheDir))
-			assert.NilError(t, err)
-			assert.Equal(t, StatusUsedCache, r.Status)
+		r, err = Download(context.Background(), "", dummyRemoteFileURL, WithExpectedDigest(dummyRemoteFileDigest), WithCacheDir(cacheDir))
+		assert.NilError(t, err)
+		assert.Equal(t, StatusUsedCache, r.Status)
 
-			localPath := filepath.Join(t.TempDir(), t.Name())
-			r, err = Download(context.Background(), localPath, dummyRemoteFileURL,
-				WithExpectedDigest(dummyRemoteFileDigest), WithCacheDir(cacheDir))
-			assert.NilError(t, err)
-			assert.Equal(t, StatusUsedCache, r.Status)
-		})
-		t.Run("parallel", func(t *testing.T) {
-			cacheDir := filepath.Join(t.TempDir(), "cache")
-			results := make(chan downloadResult, parallelDownloads)
-			for i := 0; i < parallelDownloads; i++ {
-				go func() {
-					r, err := Download(context.Background(), "", dummyRemoteFileURL,
-						WithExpectedDigest(dummyRemoteFileDigest), WithCacheDir(cacheDir))
-					results <- downloadResult{r, err}
-				}()
-			}
-			// We must process all results before cleanup.
-			for i := 0; i < parallelDownloads; i++ {
-				result := <-results
-				if result.err != nil {
-					t.Errorf("Download failed: %s", result.err)
-				} else if !slices.Contains(parallelStatus, result.r.Status) {
-					t.Errorf("Expected download status %s, got %s", parallelStatus, result.r.Status)
-				}
-			}
-		})
+		localPath := filepath.Join(t.TempDir(), t.Name())
+		r, err = Download(context.Background(), localPath, dummyRemoteFileURL, WithExpectedDigest(dummyRemoteFileDigest), WithCacheDir(cacheDir))
+		assert.NilError(t, err)
+		assert.Equal(t, StatusUsedCache, r.Status)
 	})
 	t.Run("cached", func(t *testing.T) {
 		_, err := Cached(dummyRemoteFileURL, WithExpectedDigest(dummyRemoteFileDigest))