Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for adding capabilities #156

Draft
wants to merge 26 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
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
23 changes: 21 additions & 2 deletions cmd/layers.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
var ignore string
var tarDirectory string
var permsFilepath string
var capsFilepath string
var rewritesFilepath string
var historyFilepath string
var maxLayers int
Expand Down Expand Up @@ -57,6 +58,14 @@ var layersReproducibleCmd = &cobra.Command{
os.Exit(1)
}
}
var caps []types.CapabilityPath
if capsFilepath != "" {
caps, err = readCapsFile(capsFilepath)
if err != nil {
fmt.Fprintf(os.Stderr, "%s", err)
os.Exit(1)
}
}
var rewrites []types.RewritePath
if rewritesFilepath != "" {
rewrites, err = readRewritesFile(rewritesFilepath)
Expand All @@ -74,7 +83,7 @@ var layersReproducibleCmd = &cobra.Command{
}
}

layers, err := nix.NewLayers(storepaths, maxLayers, parents, rewrites, ignore, perms, history)
layers, err := nix.NewLayers(storepaths, maxLayers, parents, rewrites, ignore, perms, caps, history)
if err != nil {
fmt.Fprintf(os.Stderr, "%s", err)
os.Exit(1)
Expand Down Expand Up @@ -116,6 +125,14 @@ var layersNonReproducibleCmd = &cobra.Command{
os.Exit(1)
}
}
var caps []types.CapabilityPath
if capsFilepath != "" {
caps, err = readCapsFile(capsFilepath)
if err != nil {
fmt.Fprintf(os.Stderr, "%s", err)
os.Exit(1)
}
}
var rewrites []types.RewritePath
if rewritesFilepath != "" {
rewrites, err = readRewritesFile(rewritesFilepath)
Expand All @@ -133,7 +150,7 @@ var layersNonReproducibleCmd = &cobra.Command{
}
}

layers, err := nix.NewLayersNonReproducible(storepaths, maxLayers, tarDirectory, parents, rewrites, ignore, perms, history)
layers, err := nix.NewLayersNonReproducible(storepaths, maxLayers, tarDirectory, parents, rewrites, ignore, perms, caps, history)
if err != nil {
fmt.Fprintf(os.Stderr, "%s", err)
os.Exit(1)
Expand Down Expand Up @@ -178,13 +195,15 @@ func init() {

layersNonReproducibleCmd.Flags().StringVarP(&rewritesFilepath, "rewrites", "", "", "A JSON file containing a list of path rewrites. Each element of the list is a JSON object with the attributes path, regex and repl: for a given path, the regex is replaced by repl.")
layersNonReproducibleCmd.Flags().StringVarP(&permsFilepath, "perms", "", "", "A JSON file containing file permissions")
layersNonReproducibleCmd.Flags().StringVarP(&capsFilepath, "caps", "", "", "A JSON file containing file capabilities")
layersNonReproducibleCmd.Flags().StringVarP(&historyFilepath, "history", "", "", "A JSON file containing layer history")
layersNonReproducibleCmd.Flags().IntVarP(&maxLayers, "max-layers", "", 1, "The maximum number of layers")

rootCmd.AddCommand(layersReproducibleCmd)
layersReproducibleCmd.Flags().StringVarP(&ignore, "ignore", "", "", "Ignore the path from the list of storepaths")
layersReproducibleCmd.Flags().StringVarP(&rewritesFilepath, "rewrites", "", "", "A JSON file containing path rewrites")
layersReproducibleCmd.Flags().StringVarP(&permsFilepath, "perms", "", "", "A JSON file containing file permissions")
layersReproducibleCmd.Flags().StringVarP(&capsFilepath, "caps", "", "", "A JSON file containing file capabilities")
layersReproducibleCmd.Flags().StringVarP(&historyFilepath, "history", "", "", "A JSON file containing layer history")
layersReproducibleCmd.Flags().IntVarP(&maxLayers, "max-layers", "", 1, "The maximum number of layers")

Expand Down
12 changes: 12 additions & 0 deletions cmd/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,18 @@ func readPermsFile(filename string) (permPaths []types.PermPath, err error) {
return
}

func readCapsFile(filename string) (capsPaths []types.CapabilityPath, err error) {
content, err := os.ReadFile(filename)
if err != nil {
return capsPaths, err
}
err = json.Unmarshal(content, &capsPaths)
if err != nil {
return capsPaths, err
}
return
}

func readRewritesFile(filename string) (rewritePaths []types.RewritePath, err error) {
content, err := os.ReadFile(filename)
if err != nil {
Expand Down
23 changes: 22 additions & 1 deletion default.nix
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,15 @@ let
# The mode is applied on a specific path. In this path subtree,
# the mode is then applied on all files matching the regex.
perms ? [],
# A list of capabilities which are set when the tar layer is
# created: these capabilities are not written to the Nix store.
#
# Each element of this permission list is a dict such as
# { path = "a store path";
# regex = ".*";
# caps = ["CAP_NET_BIND_SERVICE"];
# }
capabilities ? [],
# The maximun number of layer to create. This is based on the
# store path "popularity" as described in
# https://grahamc.com/blog/nix-and-layered-docker-images
Expand Down Expand Up @@ -284,6 +293,8 @@ let
rewritesFlag = "--rewrites ${rewritesFile}";
permsFile = pkgs.writeText "perms.json" (l.toJSON perms);
permsFlag = l.optionalString (perms != []) "--perms ${permsFile}";
capsFile = pkgs.writeText "caps.json" (l.toJSON capabilities);
capsFlag = l.optionalString (capabilities != []) "--caps ${capsFile}";
historyFile = pkgs.writeText "history.json" (l.toJSON metadata);
historyFlag = l.optionalString (metadata != {}) "--history ${historyFile}";
allDeps = deps ++ copyToRootList;
Expand All @@ -296,6 +307,7 @@ let
--max-layers ${toString maxLayers} \
${rewritesFlag} \
${permsFlag} \
${capsFlag} \
${historyFlag} \
${tarDirectory} \
${l.concatMapStringsSep " " (l: l + "/layers.json") layers} \
Expand Down Expand Up @@ -394,6 +406,15 @@ let
# The mode is applied on a specific path. In this path subtree,
# the mode is then applied on all files matching the regex.
perms ? [],
# A list of capabilities which are set when the tar layer is
# created: these capabilities are not written to the Nix store.
#
# Each element of this permission list is a dict such as
# { path = "a store path";
# regex = ".*";
# caps = ["CAP_NET_BIND_SERVICE"];
# }
capabilities ? [],
# The maximun number of layer to create. This is based on the
# store path "popularity" as described in
# https://grahamc.com/blog/nix-and-layered-docker-images
Expand Down Expand Up @@ -450,7 +471,7 @@ let
];

customizationLayer = buildLayer {
inherit maxLayers;
inherit maxLayers capabilities;
perms = perms';
copyToRoot = if initializeNixDatabase
then copyToRootList ++ [nixDatabase]
Expand Down
58 changes: 58 additions & 0 deletions examples/capabilities.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
{ pkgs, nix2container }:
let
nginxPort = "80";
nginxConf = pkgs.writeText "nginx.conf" ''
user nobody nobody;
daemon off;
error_log /dev/stdout info;
pid /dev/null;
events {}
http {
access_log /dev/stdout;
server {
listen ${nginxPort};
index index.html;
location / {
root ${nginxWebRoot};
}
}
}
'';
nginxWebRoot = pkgs.writeTextDir "index.html" ''
<html><body><h1>Hello from NGINX</h1></body></html>
'';
nginxVar = pkgs.runCommand "nginx-var" {} ''
mkdir -p $out/var/log/nginx
mkdir -p $out/var/cache/nginx
'';
in
nix2container.buildImage {
name = "nginx";

layers = [
(nix2container.buildLayer {
copyToRoot = [
pkgs.dockerTools.fakeNss
nginxVar
];
})
(nix2container.buildLayer {
copyToRoot = [
pkgs.nginx
];
capabilities = [
{
path = pkgs.nginx;
regex = "bin/nginx";
caps = [ "CAP_NET_BIND_SERVICE" ];
}
];
})
];
config = {
Cmd = [ "/bin/nginx" "-c" nginxConf ];
ExposedPorts = {
"${nginxPort}/tcp" = {};
};
};
}
1 change: 1 addition & 0 deletions examples/default.nix
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,5 @@
ownership = pkgs.callPackage ./ownership.nix { inherit nix2container; };
created = pkgs.callPackage ./created.nix { inherit nix2container; };
metadata = pkgs.callPackage ./metadata.nix { inherit nix2container; };
capabilities = pkgs.callPackage ./capabilities.nix { inherit nix2container; };
}
23 changes: 18 additions & 5 deletions nix/layers.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import (
"github.com/sirupsen/logrus"
)

func getPaths(storePaths []string, parents []types.Layer, rewrites []types.RewritePath, exclude string, permPaths []types.PermPath) types.Paths {
func getPaths(storePaths []string, parents []types.Layer, rewrites []types.RewritePath, exclude string, permPaths []types.PermPath, capPaths []types.CapabilityPath) types.Paths {
var paths types.Paths
for _, p := range storePaths {
path := types.Path{
Expand All @@ -36,6 +36,19 @@ func getPaths(storePaths []string, parents []types.Layer, rewrites []types.Rewri
if perms != nil {
pathOptions.Perms = perms
}
var caps []types.Capability
for _, cap := range capPaths {
if p == cap.Path {
hasPathOptions = true
caps = append(caps, types.Capability{
Regex: cap.Regex,
Caps: cap.Caps,
})
}
}
if caps != nil {
pathOptions.Capabilities = caps
}
for _, rewrite := range rewrites {
if p == rewrite.Path {
hasPathOptions = true
Expand Down Expand Up @@ -105,13 +118,13 @@ func newLayers(paths types.Paths, tarDirectory string, maxLayers int, history v1
return layers, nil
}

func NewLayers(storePaths []string, maxLayers int, parents []types.Layer, rewrites []types.RewritePath, exclude string, perms []types.PermPath, history v1.History) ([]types.Layer, error) {
paths := getPaths(storePaths, parents, rewrites, exclude, perms)
func NewLayers(storePaths []string, maxLayers int, parents []types.Layer, rewrites []types.RewritePath, exclude string, perms []types.PermPath, caps []types.CapabilityPath, history v1.History) ([]types.Layer, error) {
paths := getPaths(storePaths, parents, rewrites, exclude, perms, caps)
return newLayers(paths, "", maxLayers, history)
}

func NewLayersNonReproducible(storePaths []string, maxLayers int, tarDirectory string, parents []types.Layer, rewrites []types.RewritePath, exclude string, perms []types.PermPath, history v1.History) (layers []types.Layer, err error) {
paths := getPaths(storePaths, parents, rewrites, exclude, perms)
func NewLayersNonReproducible(storePaths []string, maxLayers int, tarDirectory string, parents []types.Layer, rewrites []types.RewritePath, exclude string, perms []types.PermPath, caps []types.CapabilityPath, history v1.History) (layers []types.Layer, err error) {
paths := getPaths(storePaths, parents, rewrites, exclude, perms, caps)
return newLayers(paths, tarDirectory, maxLayers, history)
}

Expand Down
6 changes: 3 additions & 3 deletions nix/layers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ func TestPerms(t *testing.T) {
Mode: "0641",
},
}
layer, err := NewLayers(paths, 1, []types.Layer{}, []types.RewritePath{}, "", perms, v1.History{})
layer, err := NewLayers(paths, 1, []types.Layer{}, []types.RewritePath{}, "", perms, []types.CapabilityPath{}, v1.History{})
if err != nil {
t.Fatalf("%v", err)
}
Expand Down Expand Up @@ -52,7 +52,7 @@ func TestNewLayers(t *testing.T) {
paths := []string{
"../data/layer1/file1",
}
layer, err := NewLayers(paths, 1, []types.Layer{}, []types.RewritePath{}, "", []types.PermPath{}, v1.History{})
layer, err := NewLayers(paths, 1, []types.Layer{}, []types.RewritePath{}, "", []types.PermPath{}, []types.CapabilityPath{}, v1.History{})
if err != nil {
t.Fatalf("%v", err)
}
Expand All @@ -72,7 +72,7 @@ func TestNewLayers(t *testing.T) {
assert.Equal(t, expected, layer)

tmpDir := t.TempDir()
layer, err = NewLayersNonReproducible(paths, 1, tmpDir, []types.Layer{}, []types.RewritePath{}, "", []types.PermPath{}, v1.History{})
layer, err = NewLayersNonReproducible(paths, 1, tmpDir, []types.Layer{}, []types.RewritePath{}, "", []types.PermPath{}, []types.CapabilityPath{}, v1.History{})
if err != nil {
t.Fatalf("%v", err)
}
Expand Down
45 changes: 45 additions & 0 deletions nix/tar.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,26 @@ func createDirectory(tw *tar.Writer, path string) error {
return nil
}

// Version 3 capability format
// struct vfs_cap_data {
// __le32 magic_etc; /* magic, version and flags */
// struct {
// __le32 permitted; /* permitted capabilities */
// __le32 inheritable; /* inheritable capabilities */
// } data[2]; /* realistically, one is enough */
// __le32 effective; /* effective capabilities */
// };
type vfsNsCapData struct {
MagicEtc uint32
Data [2]struct {
Permitted uint32
Inheritable uint32
}
Effective uint32
}

const vfsCapRevision3 = 0x03000000

func appendFileToTar(tw *tar.Writer, srcPath, dstPath string, info os.FileInfo, opts *types.PathOptions) error {
var link string
var err error
Expand Down Expand Up @@ -124,6 +144,31 @@ func appendFileToTar(tw *tar.Writer, srcPath, dstPath string, info os.FileInfo,
}
}
}


// Handle capabilities if defined
if len(opts.Capabilities) > 0 {
// Initialize PAXRecords if nil
if hdr.PAXRecords == nil {
hdr.PAXRecords = make(map[string]string)
}

for _, cap := range opts.Capabilities {
re := regexp.MustCompile(cap.Regex)
if re.Match([]byte(srcPath)) {
logrus.Infof("Regex matches!: %s path: %s", cap.Regex, srcPath)

data := NewVFSCapData(uint32(1 << CAP_NET_BIND_SERVICE), uint32(1 << CAP_NET_BIND_SERVICE), true, 0)

logrus.Infof("capabilities data: %v", data)

capBytes := data.ToBytes()
logrus.Infof("cap bytes: %v hex: %s", capBytes, string(capBytes))

hdr.PAXRecords["SCHILY.xattr.security.capability"] = string(capBytes)
}
}
}
}

hdr.ModTime = time.Date(1970, 01, 01, 0, 0, 1, 0, time.UTC)
Expand Down
Loading