feat: add deployment foundation and cross-device handoff
This commit is contained in:
@@ -0,0 +1,135 @@
|
||||
// Package composepolicy validates a deliberately restricted, offline Compose
|
||||
// profile. Passing this policy never authorizes execution or proves image safety.
|
||||
package composepolicy
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"path"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"server-deploy/internal/appbundle"
|
||||
"server-deploy/internal/wire"
|
||||
)
|
||||
|
||||
const Profile = "isolated-compose-v1"
|
||||
|
||||
type document struct {
|
||||
Services map[string]service `json:"services"`
|
||||
Networks map[string]network `json:"networks"`
|
||||
Volumes map[string]struct{} `json:"volumes"`
|
||||
}
|
||||
type network struct {
|
||||
Internal bool `json:"internal"`
|
||||
}
|
||||
type service struct {
|
||||
Image string `json:"image"`
|
||||
User string `json:"user"`
|
||||
ReadOnly bool `json:"read_only"`
|
||||
CapDrop []string `json:"cap_drop"`
|
||||
SecurityOpt []string `json:"security_opt"`
|
||||
Restart string `json:"restart"`
|
||||
Networks []string `json:"networks"`
|
||||
Volumes []mount `json:"volumes"`
|
||||
}
|
||||
type mount struct {
|
||||
Type string `json:"type"`
|
||||
Source string `json:"source"`
|
||||
Target string `json:"target"`
|
||||
ReadOnly bool `json:"read_only"`
|
||||
}
|
||||
|
||||
var identifier = regexp.MustCompile(`^[a-z][a-z0-9-]{0,47}$`)
|
||||
var pinnedImage = regexp.MustCompile(`^[a-z0-9][a-z0-9._/-]*@sha256:[0-9a-f]{64}$`)
|
||||
var targetPath = regexp.MustCompile(`^/[a-zA-Z0-9_./-]+$`)
|
||||
|
||||
// Check consumes only the authenticated entrypoint bytes and manifest supplied
|
||||
// by appbundle.Verify. It neither reads files nor renders/interpolates templates.
|
||||
// All fields in the profile are mandatory; unknown Compose features fail closed.
|
||||
func Check(manifest appbundle.Manifest, data []byte) error {
|
||||
reject := errors.New("Compose document rejected by restricted policy")
|
||||
var d document
|
||||
if wire.Decode(bytes.NewReader(data), &d, 4<<20) != nil {
|
||||
return reject
|
||||
}
|
||||
if len(manifest.Components) < 1 || len(manifest.Components) > 32 || len(d.Services) != len(manifest.Components) {
|
||||
return reject
|
||||
}
|
||||
if len(d.Networks) != 1 || !d.Networks["backend"].Internal || len(d.Volumes) > 128 {
|
||||
return reject
|
||||
}
|
||||
images := make(map[string]string, len(manifest.Components))
|
||||
for _, c := range manifest.Components {
|
||||
if !identifier.MatchString(c.Name) || !pinnedImage.MatchString(c.Image) || images[c.Name] != "" {
|
||||
return reject
|
||||
}
|
||||
images[c.Name] = c.Image
|
||||
}
|
||||
for name := range d.Volumes {
|
||||
if !identifier.MatchString(name) {
|
||||
return reject
|
||||
}
|
||||
}
|
||||
used := make(map[string]bool)
|
||||
for name, s := range d.Services {
|
||||
if images[name] == "" || s.Image != images[name] || !nonRootUser(s.User) || !s.ReadOnly {
|
||||
return reject
|
||||
}
|
||||
if !only(s.CapDrop, "ALL") || !only(s.SecurityOpt, "no-new-privileges:true") || !only(s.Networks, "backend") {
|
||||
return reject
|
||||
}
|
||||
if s.Restart != "unless-stopped" && s.Restart != "no" {
|
||||
return reject
|
||||
}
|
||||
if len(s.Volumes) > 128 {
|
||||
return reject
|
||||
}
|
||||
targets := make([]string, 0, len(s.Volumes))
|
||||
for _, v := range s.Volumes {
|
||||
if _, exists := d.Volumes[v.Source]; !exists || used[v.Source] || v.Type != "volume" {
|
||||
return reject
|
||||
}
|
||||
if len(v.Target) > 240 || !targetPath.MatchString(v.Target) || path.Clean(v.Target) != v.Target || v.Target == "/" {
|
||||
return reject
|
||||
}
|
||||
// Deny runtime/system trees as well as overlapping mounts. Only
|
||||
// application-data destinations belong in this initial profile.
|
||||
for _, protected := range []string{"/proc", "/sys", "/dev", "/etc", "/run", "/var/run", "/bin", "/sbin", "/usr", "/lib", "/lib64"} {
|
||||
if overlaps(v.Target, protected) {
|
||||
return reject
|
||||
}
|
||||
}
|
||||
for _, previous := range targets {
|
||||
if overlaps(v.Target, previous) {
|
||||
return reject
|
||||
}
|
||||
}
|
||||
targets = append(targets, v.Target)
|
||||
used[v.Source] = true
|
||||
}
|
||||
}
|
||||
if len(used) != len(d.Volumes) {
|
||||
return reject
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func only(values []string, expected string) bool { return len(values) == 1 && values[0] == expected }
|
||||
func overlaps(a, b string) bool {
|
||||
return a == b || strings.HasPrefix(a, b+"/") || strings.HasPrefix(b, a+"/")
|
||||
}
|
||||
func nonRootUser(value string) bool {
|
||||
parts := strings.Split(value, ":")
|
||||
if len(parts) != 2 {
|
||||
return false
|
||||
}
|
||||
for _, part := range parts {
|
||||
n, err := strconv.ParseUint(part, 10, 32)
|
||||
if err != nil || n == 0 || n == 4294967295 || strconv.FormatUint(n, 10) != part {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
Reference in New Issue
Block a user