From 0e8736045e6b191666ebda83caa74197e9a97d2b Mon Sep 17 00:00:00 2001 From: Mai MISHIRO <33324303+mai346@users.noreply.github.com> Date: Mon, 18 Dec 2017 22:54:32 +0900 Subject: [PATCH] LXC container support without LXD (#552) * LXC container support without LXD * Fix: LXC required root privilege * Update README --- README.ja.md | 33 +++++++++++++++++--- README.md | 35 ++++++++++++++++++--- scan/base.go | 42 +++++++++++++++++++++++++ scan/executil.go | 8 +++++ scan/executil_test.go | 72 ++++++++++++++++++++++++++++++++++++++----- 5 files changed, 173 insertions(+), 17 deletions(-) diff --git a/README.ja.md b/README.ja.md index d0d41a17..c0a59182 100644 --- a/README.ja.md +++ b/README.ja.md @@ -75,9 +75,10 @@ Table of Contents * [Example: Scan specific servers](#example-scan-specific-servers) * [Example: Scan via shell instead of SSH.](#example-scan-via-shell-instead-of-ssh) * [cronで動かす場合](#cronで動かす場合) - * [Example: Scan containers (Docker/LXD)](#example-scan-containers-dockerlxd) + * [Example: Scan containers (Docker/LXD/LXC)](#example-scan-containers-dockerlxdlxc) * [Docker](#docker) * [LXDコンテナをスキャンする場合](#lxdコンテナをスキャンする場合) + * [LXCコンテナをスキャンする場合](#lxcコンテナをスキャンする場合) * [Usage: Report](#usage-report) * [How to read a report](#how-to-read-a-report) * [Example](#example-1) @@ -734,7 +735,7 @@ host = "172.31.4.82" # ["key", "value"], #] #[servers.172-31-4-82.containers] -#type = "lxd" # or "docker" +#type = "lxd" # or "docker" or "lxc" #includes = ["${running}"] #excludes = ["container_name", "container_id"] ``` @@ -819,7 +820,7 @@ host = "172.31.4.82" # ["key", "value"], #] #[servers.172-31-4-82.containers] - #type = "lxd" # or "docker" + #type = "lxd" # or "docker" or "lxc" #includes = ["${running}"] #excludes = ["container_name", "container_id"] ``` @@ -1103,7 +1104,7 @@ RHEL/CentOSの場合、スキャン対象サーバの/etc/sudoersに以下を追 Defaults:vuls !requiretty ``` -## Example: Scan containers (Docker/LXD) +## Example: Scan containers (Docker/LXD/LXC) コンテナはSSHデーモンを起動しないで運用するケースが一般的。 @@ -1177,6 +1178,30 @@ type = "lxd" includes = ["${running}"] ``` +### LXC + +Vulsは、ホストにSSHで接続し、`lxc-attach`でLXCコンテナにコマンドを発行して脆弱性をスキャンする。 +``` +[servers] + +[servers.172-31-4-82] +host = "172.31.4.82" +user = "ec2-user" +keyPath = "/home/username/.ssh/id_rsa" + +[servers.172-31-4-82.containers] +type = "lxc" +includes = ["${running}"] +``` + +LXCコンテナの操作にはroot権限が必要です。 + +スキャン対象サーバ上の`/etc/sudoers`のサンプル + +``` +vuls ALL=(ALL) NOPASSWD:/usr/bin/lxc-attach -n *, /usr/bin/lxc-ls * +``` + # Usage: Report ``` diff --git a/README.md b/README.md index c2ff1734..c80ee3b4 100644 --- a/README.md +++ b/README.md @@ -81,9 +81,10 @@ Table of Contents * [Example: Scan specific servers](#example-scan-specific-servers) * [Example: Scan via shell instead of SSH.](#example-scan-via-shell-instead-of-ssh) * [cron](#cron) - * [Example: Scan containers (Docker/LXD)](#example-scan-containers-dockerlxd) + * [Example: Scan containers (Docker/LXD/LXC)](#example-scan-containers-dockerlxdlxc) * [Docker](#docker) * [LXD](#lxd) + * [LXC](#lxc) * [Usage: Report](#usage-report) * [How to read a report](#how-to-read-a-report) * [Example](#example-1) @@ -747,7 +748,7 @@ host = "172.31.4.82" # ["key", "value"], #] #[servers.172-31-4-82.containers] -#type = "lxd" # or "docker" +#type = "lxd" # or "docker" or "lxc" #includes = ["${running}"] #excludes = ["container_name", "container_id"] ``` @@ -852,7 +853,7 @@ You can customize your configuration using this template. # ["key", "value"], #] #[servers.172-31-4-82.containers] - #type = "lxd" # or "docker" + #type = "lxd" # or "docker" or "lxc" #includes = ["${running}"] #excludes = ["container_name", "container_id"] ``` @@ -867,7 +868,7 @@ You can customize your configuration using this template. - cpeNames: see [Usage: Scan vulnerability of non-OS package](#usage-scan-vulnerability-of-non-os-package) - ignoreCves: CVE IDs that will not be reported. But output to JSON file. - optional: Add additional information to JSON report. - - containers: see [Example: Scan containers (Docker/LXD)(#example-scan-containers-dockerlxd) + - containers: see [Example: Scan containers (Docker/LXD/LXC)(#example-scan-containers-dockerlxdlxc) Vuls supports two types of SSH. One is external command. The other is native go implementation. For details, see [-ssh-native-insecure option](#-ssh-native-insecure-option) @@ -1108,7 +1109,7 @@ If you use local scan mode for cron jobs, don't forget to add below line to `/et Defaults:vuls !requiretty ``` -## Example: Scan containers (Docker/LXD) +## Example: Scan containers (Docker/LXD/LXC) It is common that keep containers running without SSHd daemon. see [Docker Blog:Why you don't need to run SSHd in your Docker containers](https://blog.docker.com/2014/06/why-you-dont-need-to-run-sshd-in-docker/) @@ -1182,6 +1183,30 @@ type = "lxd" includes = ["${running}"] ``` +### LXC + +Vuls scans lxc via `lxc-attach` instead of SSH. +``` +[servers] + +[servers.172-31-4-82] +host = "172.31.4.82" +user = "ec2-user" +keyPath = "/home/username/.ssh/id_rsa" + +[servers.172-31-4-82.containers] +type = "lxc" +includes = ["${running}"] +``` + +LXC required root privilege. + +Example of /etc/sudoers on target servers + +``` +vuls ALL=(ALL) NOPASSWD:/usr/bin/lxc-attach -n *, /usr/bin/lxc-ls * +``` + ---- # Usage: Report diff --git a/scan/base.go b/scan/base.go index 8aa42370..81a461d7 100644 --- a/scan/base.go +++ b/scan/base.go @@ -110,6 +110,12 @@ func (l *base) allContainers() (containers []config.Container, err error) { return containers, err } return l.parseLxdPs(stdout) + case "lxc": + stdout, err := l.lxcPs("-1") + if err != nil { + return containers, err + } + return l.parseLxcPs(stdout) default: return containers, fmt.Errorf( "Not supported yet: %s", l.ServerInfo.Containers.Type) @@ -130,6 +136,12 @@ func (l *base) runningContainers() (containers []config.Container, err error) { return containers, err } return l.parseLxdPs(stdout) + case "lxc": + stdout, err := l.lxcPs("-1 --running") + if err != nil { + return containers, err + } + return l.parseLxcPs(stdout) default: return containers, fmt.Errorf( "Not supported yet: %s", l.ServerInfo.Containers.Type) @@ -150,6 +162,12 @@ func (l *base) exitedContainers() (containers []config.Container, err error) { return containers, err } return l.parseLxdPs(stdout) + case "lxc": + stdout, err := l.lxcPs("-1 --stopped") + if err != nil { + return containers, err + } + return l.parseLxcPs(stdout) default: return containers, fmt.Errorf( "Not supported yet: %s", l.ServerInfo.Containers.Type) @@ -174,6 +192,15 @@ func (l *base) lxdPs(option string) (string, error) { return r.Stdout, nil } +func (l *base) lxcPs(option string) (string, error) { + cmd := fmt.Sprintf("lxc-ls %s 2>/dev/null", option) + r := l.exec(cmd, sudo) + if !r.isSuccess() { + return "", fmt.Errorf("failed to SSH: %s", r) + } + return r.Stdout, nil +} + func (l *base) parseDockerPs(stdout string) (containers []config.Container, err error) { lines := strings.Split(stdout, "\n") for _, line := range lines { @@ -214,6 +241,21 @@ func (l *base) parseLxdPs(stdout string) (containers []config.Container, err err return } +func (l *base) parseLxcPs(stdout string) (containers []config.Container, err error) { + lines := strings.Split(stdout, "\n") + for _, line := range lines { + fields := strings.Fields(line) + if len(fields) == 0 { + break + } + containers = append(containers, config.Container{ + ContainerID: fields[0], + Name: fields[0], + }) + } + return +} + func (l *base) detectPlatform() { ok, instanceID, err := l.detectRunningOnAws() if err != nil { diff --git a/scan/executil.go b/scan/executil.go index 77ba6553..c7c79b92 100644 --- a/scan/executil.go +++ b/scan/executil.go @@ -365,8 +365,16 @@ func decorateCmd(c conf.ServerInfo, cmd string, sudo bool) string { cmd = fmt.Sprintf(`docker exec --user 0 %s %s -c '%s'`, c.Container.ContainerID, dockerShell(c.Distro.Family), cmd) case "lxd": + // If the user belong to the "lxd" group, root privilege is not required. cmd = fmt.Sprintf(`lxc exec %s -- %s -c '%s'`, c.Container.Name, dockerShell(c.Distro.Family), cmd) + case "lxc": + cmd = fmt.Sprintf(`lxc-attach -n %s 2>/dev/null -- %s -c '%s'`, + c.Container.Name, dockerShell(c.Distro.Family), cmd) + // LXC required root privilege + if c.User != "root" { + cmd = fmt.Sprintf("sudo -S %s", cmd) + } } } // cmd = fmt.Sprintf("set -x; %s", cmd) diff --git a/scan/executil_test.go b/scan/executil_test.go index 5ff86b9f..cd8c1c1f 100644 --- a/scan/executil_test.go +++ b/scan/executil_test.go @@ -46,21 +46,21 @@ func TestDecorateCmd(t *testing.T) { }, // non-root sudo false { - conf: config.ServerInfo{User: "non-roor"}, + conf: config.ServerInfo{User: "non-root"}, cmd: "ls", sudo: false, expected: "ls", }, // non-root sudo true { - conf: config.ServerInfo{User: "non-roor"}, + conf: config.ServerInfo{User: "non-root"}, cmd: "ls", sudo: true, expected: "sudo -S ls", }, // non-root sudo true { - conf: config.ServerInfo{User: "non-roor"}, + conf: config.ServerInfo{User: "non-root"}, cmd: "ls | grep hoge", sudo: true, expected: "sudo -S ls | grep hoge", @@ -70,7 +70,7 @@ func TestDecorateCmd(t *testing.T) { { conf: config.ServerInfo{ User: "root", - Container: config.Container{ContainerID: "abc"}, + Container: config.Container{ContainerID: "abc", Name: "def"}, Containers: config.Containers{Type: "docker"}, }, cmd: "ls", @@ -81,7 +81,7 @@ func TestDecorateCmd(t *testing.T) { { conf: config.ServerInfo{ User: "root", - Container: config.Container{ContainerID: "abc"}, + Container: config.Container{ContainerID: "abc", Name: "def"}, Containers: config.Containers{Type: "docker"}, }, cmd: "ls", @@ -92,7 +92,7 @@ func TestDecorateCmd(t *testing.T) { { conf: config.ServerInfo{ User: "non-root", - Container: config.Container{ContainerID: "abc"}, + Container: config.Container{ContainerID: "abc", Name: "def"}, Containers: config.Containers{Type: "docker"}, }, cmd: "ls", @@ -103,7 +103,7 @@ func TestDecorateCmd(t *testing.T) { { conf: config.ServerInfo{ User: "non-root", - Container: config.Container{ContainerID: "abc"}, + Container: config.Container{ContainerID: "abc", Name: "def"}, Containers: config.Containers{Type: "docker"}, }, cmd: "ls", @@ -114,7 +114,7 @@ func TestDecorateCmd(t *testing.T) { { conf: config.ServerInfo{ User: "non-root", - Container: config.Container{ContainerID: "abc"}, + Container: config.Container{ContainerID: "abc", Name: "def"}, Containers: config.Containers{Type: "docker"}, }, cmd: "ls | grep hoge", @@ -177,6 +177,62 @@ func TestDecorateCmd(t *testing.T) { sudo: true, expected: `lxc exec def -- /bin/sh -c 'ls | grep hoge'`, }, + // -------------lxc------------- + // root sudo false lxc + { + conf: config.ServerInfo{ + User: "root", + Container: config.Container{ContainerID: "abc", Name: "def"}, + Containers: config.Containers{Type: "lxc"}, + }, + cmd: "ls", + sudo: false, + expected: `lxc-attach -n def 2>/dev/null -- /bin/sh -c 'ls'`, + }, + // root sudo true lxc + { + conf: config.ServerInfo{ + User: "root", + Container: config.Container{ContainerID: "abc", Name: "def"}, + Containers: config.Containers{Type: "lxc"}, + }, + cmd: "ls", + sudo: true, + expected: `lxc-attach -n def 2>/dev/null -- /bin/sh -c 'ls'`, + }, + // non-root sudo false, lxc + { + conf: config.ServerInfo{ + User: "non-root", + Container: config.Container{ContainerID: "abc", Name: "def"}, + Containers: config.Containers{Type: "lxc"}, + }, + cmd: "ls", + sudo: false, + expected: `sudo -S lxc-attach -n def 2>/dev/null -- /bin/sh -c 'ls'`, + }, + // non-root sudo true, lxc + { + conf: config.ServerInfo{ + User: "non-root", + Container: config.Container{ContainerID: "abc", Name: "def"}, + Containers: config.Containers{Type: "lxc"}, + }, + cmd: "ls", + sudo: true, + expected: `sudo -S lxc-attach -n def 2>/dev/null -- /bin/sh -c 'ls'`, + }, + // non-root sudo true lxc + { + conf: config.ServerInfo{ + User: "non-root", + Container: config.Container{ContainerID: "abc", Name: "def"}, + Containers: config.Containers{Type: "lxc"}, + }, + cmd: "ls | grep hoge", + sudo: true, + expected: `sudo -S lxc-attach -n def 2>/dev/null -- /bin/sh -c 'ls | grep hoge'`, + }, } for _, tt := range tests {