Skip to content
This repository has been archived by the owner on Oct 26, 2023. It is now read-only.

Commit

Permalink
Improved CNI integration + doc, fixed exec options, removed useless l…
Browse files Browse the repository at this point in the history
…ock dir creation

Signed-off-by: Max Goltzsche <[email protected]>
  • Loading branch information
mgoltzsche committed Nov 18, 2018
1 parent 9e8bebd commit a474787
Show file tree
Hide file tree
Showing 15 changed files with 280 additions and 82 deletions.
16 changes: 10 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ Concerning accessibility, usability and security a rootless container engine has
- **Containers can be run by unprivileged users.**
_Required in restrictive environments and useful for graphical applications._
- **Container images can be built in almost every Linux environment.**
_More flexibility in unprivileged builds - nesting containers is still limited (see [experiments](experiments.md))._
_More flexibility in unprivileged builds - nesting containers is also possible (see [experiments and limitations](nested-containers.md))._
- **A higher degree and more flexible level of security.**
_Less likely for an attacker to gain root access when run as unprivileged user._
_User/group-based container access control._
Expand All @@ -38,16 +38,20 @@ Concerning accessibility, usability and security a rootless container engine has
Container execution as unprivileged user is limited:


**Container networks cannot be configured.**
As a result in a restrictive environment without root access only the host network can be used.
As a workaround ports can be mapped on the host network using [PRoot](https://github.com/rootless-containers/PRoot)*.
Alternatively a daemon process could manage networks for unprivileged users (TBD).
**Container networking is limited.**
With plain ctnr/runc only the host network can be used.
The standard [CNI plugins](https://github.com/containernetworking/plugins) require root privileges.
One workaround is to map ports on the host network using [PRoot](https://github.com/rootless-containers/PRoot)* accepting bad performance.
A better solution is to use [slirp4netns](https://github.com/rootless-containers/slirp4netns) which emulates the TCP/IP stack in a user namespace efficiently.
It can be used with ctnr via the [slirp-cni-plugin](https://github.com/mgoltzsche/slirp-cni-plugin).
Once container initialization is also moved into a user namespace with slirp the standard CNI plugins can be used again.
For instance the [bridge](https://github.com/containernetworking/plugins/tree/master/plugins/main/bridge) can be used to achieve communication between containers (see [user-mode networking](user-mode-networking.md)).


**Inside the container a process' or file's user cannot be changed.**
This is caused by the fact that all operations in the container are still run by the host user (who is just mapped to user 0 inside the container).
Unfortunately this stops many package managers as well as official docker images from working:
While `apk` already works with plain [runc](https://github.com/opencontainers/runc) `apt-get` does not since it requires to change a user permanently.
While `apk` or `dnf` already work with plain [runc](https://github.com/opencontainers/runc) `apt-get` does not since it requires to change a user permanently.
To overcome this limitation ctnr supports the `user.rootlesscontainers` xattr and integrates with [PRoot](https://github.com/rootless-containers/PRoot)*.


Expand Down
13 changes: 4 additions & 9 deletions bundle/builder/hook.go
Original file line number Diff line number Diff line change
Expand Up @@ -128,21 +128,16 @@ func (b *HookBuilder) Build(spec *generate.Generator) (err error) {
}
cniPluginPaths := os.Getenv("CNI_PATH")
if cniPluginPaths == "" {
pluginPath := filepath.Join(filepath.Dir(executable), "..", "cni-plugins")
if s, err := os.Stat(pluginPath); err == nil && s.IsDir() {
cniPluginPaths = pluginPath
cniPluginPaths = filepath.Join(filepath.Dir(executable), "..", "cni-plugins")
if s, err := os.Stat(cniPluginPaths); err != nil || !s.IsDir() {
return errors.New("CNI plugin directory cannot be derived from executable (../cni-plugins) and CNI_PATH env var is not specified. See https://github.com/containernetworking/cni/blob/master/SPEC.md")
}
}
if cniPluginPaths == "" {
return errors.New("CNI_PATH environment variable empty. It must contain paths to CNI plugins. See https://github.com/containernetworking/cni/blob/master/SPEC.md")
}
// TODO: add all CNI env vars
cniEnv := []string{
"PATH=" + os.Getenv("PATH"),
"CNI_PATH=" + cniPluginPaths,
}
netConfPath := os.Getenv("NETCONFPATH")
if netConfPath != "" {
if netConfPath := os.Getenv("NETCONFPATH"); netConfPath != "" {
cniEnv = append(cniEnv, "NETCONFPATH="+netConfPath)
}
ipamDataDir := b.hook.IPAMDataDir
Expand Down
22 changes: 21 additions & 1 deletion cmd/net.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (

"github.com/containernetworking/cni/libcni"
"github.com/mgoltzsche/ctnr/net"
"github.com/mitchellh/go-homedir"
specs "github.com/opencontainers/runtime-spec/specs-go"
"github.com/pkg/errors"
"github.com/spf13/cobra"
Expand Down Expand Up @@ -154,13 +155,14 @@ func applyArgs(cfg *net.ConfigFileGenerator) {
}

func loadNetConfigs(args []string) (r []*libcni.NetworkConfigList, err error) {
networks, err := net.NewNetConfigs("")
netConfPath, err := getNetConfPath()
if err != nil {
return
}
if len(args) == 0 && len(flagPorts) > 0 {
return nil, errors.New("Cannot publish a port without a container network! Please remove the --publish option or add --network")
}
networks := net.NewNetConfigs(netConfPath)
r = make([]*libcni.NetworkConfigList, len(args))
for i, name := range args {
cfg, err := networks.GetConfig(name)
Expand All @@ -179,6 +181,24 @@ func loadNetConfigs(args []string) (r []*libcni.NetworkConfigList, err error) {
return
}

func getNetConfPath() (confDir string, err error) {
if confDir = os.Getenv("NETCONFPATH"); confDir == "" {
var homeDir string
homeDir, err = homedir.Dir()
if err != nil {
return "", errors.Wrap(err, "derive NETCONFPATH from home dir")
}
confDir = filepath.Join(homeDir, ".cni/net.d")
if _, e := os.Stat(confDir); e != nil {
// fall back to global cni conf dir when user conf dir does not exist
confDir = "/etc/cni/net.d"
}
} else if confDir, err = homedir.Expand(confDir); err != nil {
err = errors.Wrap(err, "expand NETCONFPATH")
}
return
}

func readContainerState() (s *specs.State, err error) {
s = &specs.State{}
// Read hook data from stdin
Expand Down
7 changes: 4 additions & 3 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,6 @@ func init() {
// Cobra supports persistent flags, which, if defined here,
// will be global for your application.
//RootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.ctnr.yaml)")

logrus.SetLevel(logrus.DebugLevel)
logger = logrus.New()
logger.Level = logrus.DebugLevel
Expand Down Expand Up @@ -133,12 +132,14 @@ func preRun(cmd *cobra.Command, args []string) {
// TODO: add docker auth
//DockerAuthConfig: dockerAuth,
}
var err error
if flagRootless && ctx.DockerCertPath == "" {
ctx.DockerCertPath = "./docker-certs"
}

var imagePolicy istore.TrustPolicyContext
var (
imagePolicy istore.TrustPolicyContext
err error
)
if flagImagePolicy == "reject" {
imagePolicy = istore.TrustPolicyReject()
} else if flagImagePolicy == "insecure" {
Expand Down
19 changes: 10 additions & 9 deletions cmd/serviceflags.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,6 @@ func (c *bundleFlags) InitContainerFlags(f *pflag.FlagSet) {
f.BoolVar(&c.privileged, "privileged", false, "give extended privileges to the container")
f.BoolVar(&c.proot, "proot", false, "enables PRoot")
initNetConfFlags(f, &c.netCfg)
// Stop parsing after first non flag argument (which is the image)
f.SetInterspersed(false)
}

func (c *bundleFlags) InitRunFlags(f *pflag.FlagSet) {
Expand All @@ -75,6 +73,8 @@ func (c *bundleFlags) InitProcessFlags(f *pflag.FlagSet) {
f.BoolVarP(&c.tty, "tty", "t", false, "binds a terminal to the container")
f.Var((*cCapAdd)(c), "cap-add", "add process capability ('all' adds all)")
f.Var((*cCapDrop)(c), "cap-drop", "drop process capability ('all' drops all)")
// Stop parsing after first non flag argument (either <IMAGE> <CMD> or <CONTAINERID> <CMD>)
f.SetInterspersed(false)
}

func initNetConfFlags(f *pflag.FlagSet, c *netCfg) {
Expand Down Expand Up @@ -108,15 +108,11 @@ func (c *bundleFlags) Read() (*model.Service, error) {
s.NetConf = c.net
s.Tty = c.tty
s.StdinOpen = c.stdin
s.Privileged = c.privileged
s.ReadOnly = c.readonly
s.NoPivot = c.noPivot
s.NoNewKeyring = c.noNewKeyring
s.PRoot = c.proot
if c.privileged {
s.CapAdd = append(s.CapAdd, "SYS_ADMIN")
s.MountCgroups = "rw"
s.Seccomp = "unconfined"
}
c.app = nil
c.net = model.NetConf{}
return s, nil
Expand Down Expand Up @@ -544,13 +540,18 @@ func (c *cPortBinding) String() string {
type cNetworks netCfg

func (c *cNetworks) Set(s string) error {
return addStringEntries(s, &(*netCfg)(c).net.Networks)
if s == "" {
return errors.New("no network names provided")
}
n := &(*netCfg)(c).net.Networks
*n = append(*n, strings.Split(s, ",")...)
return nil
}

func (c *cNetworks) Type() string {
return "string..."
}

func (c *cNetworks) String() string {
return entriesToString((*netCfg)(c).net.Networks)
return strings.Join((*netCfg)(c).net.Networks, ",")
}
1 change: 0 additions & 1 deletion model/compose/full-example.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@
"NET_ADMIN",
"SYS_ADMIN"
],
"seccomp": "default",
"hostname": "my-web-container",
"domainname": "foo.com",
"dns": [
Expand Down
54 changes: 36 additions & 18 deletions model/oci/ocitransform.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,22 @@ func ToSpec(service *model.Service, res model.ResourceResolver, rootless bool, i
return
}

if service.MountCgroups != "" {
if err = spec.AddCgroupsMount(service.MountCgroups); err != nil {
// privileged
seccomp := service.Seccomp
cgroupsMount := service.MountCgroups
if service.Privileged {
if cgroupsMount == "" {
cgroupsMount = "rw"
}
if seccomp == "" {
seccomp = "unconfined"
}
spec.AddBindMount("/dev/net", "/dev/net", []string{"bind"})
}

// Mount cgroups
if cgroupsMount != "" {
if err = spec.AddCgroupsMount(cgroupsMount); err != nil {
return
}
}
Expand Down Expand Up @@ -83,16 +97,16 @@ func ToSpec(service *model.Service, res model.ResourceResolver, rootless bool, i
}

// Seccomp
if service.Seccomp == "" || service.Seccomp == "default" {
if seccomp == "" || seccomp == "default" {
// Derive seccomp configuration (must be called as last)
spec.SetLinuxSeccompDefault()
} else if service.Seccomp == "unconfined" {
} else if seccomp == "unconfined" {
// Do not restrict operations with seccomp
spec.SetLinuxSeccompUnconfined()
} else {
// Use seccomp configuration from file
var j []byte
if j, err = ioutil.ReadFile(res.ResolveFile(service.Seccomp)); err != nil {
if j, err = ioutil.ReadFile(res.ResolveFile(seccomp)); err != nil {
return
}
seccomp := &specs.LinuxSeccomp{}
Expand Down Expand Up @@ -275,20 +289,24 @@ func ToSpecProcess(p *model.Process, prootPath string, builder *builder.SpecBuil
builder.SetProcessUser(idutils.User{p.User.User, p.User.Group})
}

// Privileged
capAdd := p.CapAdd
if p.Privileged {
capAdd = []string{"ALL"}
}

// Capabilities
if p.CapAdd != nil {
for _, addCap := range p.CapAdd {
if strings.ToUpper(addCap) == "ALL" {
builder.AddAllProcessCapabilities()
break
} else if err = builder.AddProcessCapability("CAP_" + addCap); err != nil {
return
}
for _, addCap := range capAdd {
if strings.ToUpper(addCap) == "ALL" {
builder.AddAllProcessCapabilities()
break
} else if err = builder.AddProcessCapability("CAP_" + addCap); err != nil {
return
}
for _, dropCap := range p.CapDrop {
if err = builder.DropProcessCapability("CAP_" + dropCap); err != nil {
return
}
}
for _, dropCap := range p.CapDrop {
if err = builder.DropProcessCapability("CAP_" + dropCap); err != nil {
return
}
}

Expand Down Expand Up @@ -318,7 +336,7 @@ func toMounts(mounts []model.VolumeMount, res model.ResourceResolver, spec *buil
// Apply default mount options. See man7.org/linux/man-pages/man8/mount.8.html
opts = []string{"bind", "nodev", "mode=0755"}
} else {
sliceutils.AddToSet(&opts, "rbind")
sliceutils.AddToSet(&opts, "bind")
}

sp := spec.Generator.Spec()
Expand Down
3 changes: 2 additions & 1 deletion model/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ type Process struct {
Cwd string `json:"working_dir,omitempty"`
Environment map[string]string `json:"environment,omitempty"`
User *User `json:"user,omitempty"`
Privileged bool `json:"privileged",omitempty"`
CapAdd []string `json:"cap_add,omitempty"`
CapDrop []string `json:"cap_drop,omitempty"`
StdinOpen bool `json:"stdin_open,omitempty"`
Expand Down Expand Up @@ -180,7 +181,7 @@ type Check struct {
}

func NewService(name string) Service {
return Service{Name: name, Seccomp: "default"}
return Service{Name: name}
}

func (c *CompoundServices) JSON() string {
Expand Down
File renamed without changes.
Loading

0 comments on commit a474787

Please sign in to comment.