github.com/mgoltzsche/ctnr@v0.7.1-alpha/bundle/builder/specbuilder.go (about) 1 // Copyright © 2017 Max Goltzsche 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package builder 16 17 import ( 18 "os" 19 "sort" 20 "strconv" 21 "strings" 22 "time" 23 24 "github.com/mgoltzsche/ctnr/pkg/idutils" 25 ispecs "github.com/opencontainers/image-spec/specs-go/v1" 26 "github.com/opencontainers/runc/libcontainer/specconv" 27 rspecs "github.com/opencontainers/runtime-spec/specs-go" 28 "github.com/opencontainers/runtime-tools/generate" 29 "github.com/opencontainers/runtime-tools/generate/seccomp" 30 "github.com/pkg/errors" 31 "github.com/syndtr/gocapability/capability" 32 ) 33 34 type SpecBuilder struct { 35 generate.Generator 36 entrypoint []string 37 cmd []string 38 user idutils.User 39 customSeccomp bool 40 proot *prootOptions 41 rootless bool 42 } 43 44 type prootOptions struct { 45 Path string 46 Ports []string 47 } 48 49 func NewSpecBuilder() SpecBuilder { 50 return SpecBuilder{Generator: generate.New()} 51 } 52 53 func FromSpec(spec *rspecs.Spec) SpecBuilder { 54 user := idutils.User{"0", "0"} 55 if spec.Process != nil { 56 user.User = strconv.Itoa(int(spec.Process.User.UID)) 57 user.Group = strconv.Itoa(int(spec.Process.User.GID)) 58 // TODO: map additional gids 59 } 60 return SpecBuilder{Generator: generate.NewFromSpec(spec)} 61 } 62 63 func (b *SpecBuilder) ToRootless() { 64 specconv.ToRootless(b.Generator.Spec()) 65 b.rootless = true 66 } 67 68 func (b *SpecBuilder) UseHostNetwork() { 69 b.RemoveLinuxNamespace(rspecs.NetworkNamespace) 70 b.SetHostname("") // empty hostname results in host's hostname 71 opts := []string{"bind", "mode=0444", "nosuid", "noexec", "nodev", "ro"} 72 b.AddBindMount("/etc/hosts", "/etc/hosts", opts) 73 b.AddBindMount("/etc/resolv.conf", "/etc/resolv.conf", opts) 74 } 75 76 func (b *SpecBuilder) SetProcessUser(user idutils.User) { 77 b.user = user 78 } 79 80 func (b *SpecBuilder) AddAllProcessCapabilities() { 81 // Add all capabilities 82 all := capability.List() 83 caps := make([]string, len(all)) 84 for i, c := range all { 85 caps[i] = "CAP_" + strings.ToUpper(c.String()) 86 } 87 c := b.Generator.Spec().Process.Capabilities 88 c.Effective = caps 89 c.Permitted = caps 90 c.Bounding = caps 91 c.Ambient = caps 92 c.Inheritable = caps 93 } 94 95 func (b *SpecBuilder) DropAllProcessCapabilities() { 96 caps := []string{} 97 c := b.Generator.Spec().Process.Capabilities 98 c.Effective = caps 99 c.Permitted = caps 100 c.Bounding = caps 101 c.Ambient = caps 102 c.Inheritable = caps 103 } 104 105 // Derives a sane default seccomp profile from the current spec. 106 // See https://github.com/jessfraz/blog/blob/master/content/post/how-to-use-new-docker-seccomp-profiles.md 107 // and https://github.com/jessfraz/docker/blob/52f32818df8bad647e4c331878fa44317e724939/docs/security/seccomp.md 108 func (b *SpecBuilder) SetLinuxSeccompDefault() { 109 spec := b.Generator.Spec() 110 spec.Linux.Seccomp = seccomp.DefaultProfile(spec) 111 } 112 113 func (b *SpecBuilder) SetLinuxSeccompUnconfined() { 114 spec := b.Generator.Spec() 115 profile := seccomp.DefaultProfile(spec) 116 profile.DefaultAction = rspecs.ActAllow 117 profile.Syscalls = nil 118 spec.Linux.Seccomp = profile 119 b.customSeccomp = true 120 } 121 122 func (b *SpecBuilder) SetLinuxSeccomp(profile *rspecs.LinuxSeccomp) { 123 spec := b.Generator.Spec() 124 if spec.Linux == nil { 125 spec.Linux = &rspecs.Linux{} 126 } 127 spec.Linux.Seccomp = profile 128 b.customSeccomp = true 129 } 130 131 func (b *SpecBuilder) AddExposedPorts(ports []string) { 132 // Merge exposedPorts annotation 133 exposedPortsAnn := "" 134 spec := b.Generator.Spec() 135 if spec.Annotations != nil { 136 exposedPortsAnn = spec.Annotations["org.opencontainers.image.exposedPorts"] 137 } 138 exposed := map[string]bool{} 139 if exposedPortsAnn != "" { 140 for _, exposePortStr := range strings.Split(exposedPortsAnn, ",") { 141 exposed[strings.Trim(exposePortStr, " ")] = true 142 } 143 } 144 for _, e := range ports { 145 exposed[strings.Trim(e, " ")] = true 146 } 147 if len(exposed) > 0 { 148 exposecsv := make([]string, len(exposed)) 149 i := 0 150 for k := range exposed { 151 exposecsv[i] = k 152 i++ 153 } 154 sort.Strings(exposecsv) 155 b.AddAnnotation("org.opencontainers.image.exposedPorts", strings.Join(exposecsv, ",")) 156 } 157 } 158 159 func (b *SpecBuilder) SetPRootPath(prootPath string) { 160 if b.proot == nil { 161 b.proot = &prootOptions{} 162 } 163 b.proot.Path = prootPath 164 // This has been derived from https://github.com/AkihiroSuda/runrootless/blob/b9a7df0120a7fee15c0223fd0fbc8c3885edd9b3/bundle/spec.go 165 b.AddTmpfsMount("/dev/proot", []string{"exec", "mode=755", "size=32256k"}) 166 b.AddBindMount(prootPath, "/dev/proot/proot", []string{"bind", "ro"}) 167 b.AddProcessEnv("PROOT_TMP_DIR", "/dev/proot") 168 b.AddProcessEnv("PROOT_NO_SECCOMP", "1") 169 b.AddProcessCapability("CAP_" + capability.CAP_SYS_PTRACE.String()) 170 } 171 172 func (b *SpecBuilder) AddPRootPortMapping(published, target string) { 173 if b.proot == nil { 174 b.proot = &prootOptions{} 175 } 176 b.proot.Ports = append(b.proot.Ports, published+":"+target) 177 } 178 179 func (b *SpecBuilder) SetProcessEntrypoint(v []string) { 180 b.entrypoint = v 181 b.cmd = nil 182 } 183 184 func (b *SpecBuilder) SetProcessCmd(v []string) { 185 b.cmd = v 186 } 187 188 func (b *SpecBuilder) applyEntrypoint() { 189 var args []string 190 if b.entrypoint != nil || b.cmd != nil { 191 if b.entrypoint != nil && b.cmd != nil { 192 args = append(b.entrypoint, b.cmd...) 193 } else if b.entrypoint != nil { 194 args = b.entrypoint 195 } else { 196 args = b.cmd 197 } 198 } else { 199 args = []string{} 200 } 201 if b.proot != nil { 202 prootArgs := []string{"/dev/proot/proot", "--kill-on-exit", "-n"} 203 user := b.user.String() 204 if user == "0:0" { 205 prootArgs = append(prootArgs, "-0") 206 } else { 207 prootArgs = append(prootArgs, "-i", b.user.String()) 208 } 209 for _, port := range b.proot.Ports { 210 prootArgs = append(prootArgs, "-p", port) 211 } 212 args = append(prootArgs, args...) 213 } 214 b.SetProcessArgs(args) 215 } 216 217 // See image to runtime spec conversion rules: https://github.com/opencontainers/image-spec/blob/master/conversion.md 218 func (b *SpecBuilder) ApplyImage(img *ispecs.Image) { 219 cfg := &img.Config 220 221 // User 222 b.user = idutils.ParseUser(img.Config.User) 223 224 // Entrypoint 225 b.SetProcessEntrypoint(cfg.Entrypoint) 226 b.SetProcessCmd(cfg.Cmd) 227 228 // Env 229 if len(cfg.Env) > 0 { 230 for _, e := range cfg.Env { 231 kv := strings.SplitN(e, "=", 2) 232 k := kv[0] 233 v := "" 234 if len(kv) == 2 { 235 v = kv[1] 236 } 237 b.AddProcessEnv(k, v) 238 } 239 } 240 241 // Working dir 242 if cfg.WorkingDir != "" { 243 b.SetProcessCwd(cfg.WorkingDir) 244 } 245 246 // Annotations 247 if cfg.Labels != nil { 248 for k, v := range cfg.Labels { 249 b.AddAnnotation(k, v) 250 } 251 } 252 // TODO: extract annotations also from image index and manifest 253 if img.Author != "" { 254 b.AddAnnotation("org.opencontainers.image.author", img.Author) 255 } 256 if img.Created != nil && !time.Unix(0, 0).Equal(*img.Created) { 257 b.AddAnnotation("org.opencontainers.image.created", (*img.Created).String()) 258 } 259 if img.Config.StopSignal != "" { 260 b.AddAnnotation("org.opencontainers.image.stopSignal", img.Config.StopSignal) 261 } 262 if cfg.ExposedPorts != nil { 263 ports := make([]string, len(cfg.ExposedPorts)) 264 i := 0 265 for k := range cfg.ExposedPorts { 266 ports[i] = k 267 i++ 268 } 269 b.AddAnnotation("org.opencontainers.image.exposedPorts", strings.Join(ports, ",")) 270 } 271 } 272 273 // Returns the generated spec with resolved user/group names 274 func (b *SpecBuilder) Spec(rootfs string) (spec *rspecs.Spec, err error) { 275 // Resolve user name 276 usr, err := b.user.Resolve(rootfs) 277 if err != nil { 278 return 279 } 280 b.user = usr.User() 281 if usr.Uid > 1<<32 { 282 return nil, errors.Errorf("uid %d exceeds range", usr.Uid) 283 } 284 if usr.Gid > 1<<32 { 285 return nil, errors.Errorf("gid %d exceeds range", usr.Gid) 286 } 287 288 // Check uid/gid constraints and proot support 289 if b.proot != nil { 290 if b.proot.Path == "" { 291 return nil, errors.New("proot user or port mappings specified but no proot path provided") 292 } 293 usr = idutils.UserIds{} // use 0 in native mapping 294 } else if b.rootless && (usr.Uid != 0 || usr.Gid != 0) { 295 return nil, errors.Errorf("rootless container: only user 0:0 supported but %s provided. hint: enable proot as a workaround", b.user.String()) 296 } 297 298 // Apply entrypoint/command (using proot) 299 b.applyEntrypoint() 300 301 // Apply process uid/gid 302 b.SetProcessUID(uint32(usr.Uid)) 303 b.SetProcessGID(uint32(usr.Gid)) 304 // TODO: set additional gids 305 306 // Apply native process uid/gid mapping 307 if b.rootless { 308 b.ClearLinuxUIDMappings() 309 b.ClearLinuxGIDMappings() 310 b.AddLinuxUIDMapping(uint32(os.Geteuid()), uint32(usr.Uid), 1) 311 b.AddLinuxGIDMapping(uint32(os.Getegid()), uint32(usr.Gid), 1) 312 } 313 314 // Generate default seccomp profile 315 if !b.customSeccomp { 316 b.SetLinuxSeccompDefault() 317 } 318 319 return b.Generator.Spec(), nil 320 } 321 322 func containsNamespace(ns rspecs.LinuxNamespaceType, l []rspecs.LinuxNamespace) bool { 323 for _, e := range l { 324 if e.Type == ns { 325 return true 326 } 327 } 328 return false 329 }