-#!/bin/bash
+#!/bin/sh
+# Client script for LXC container images.
#
-# lxc: linux Container library
-
-# Authors:
-# Daniel Lezcano <daniel.lezcano@free.fr>
-
-# This library is free software; you can redistribute it and/or
-# modify it under the terms of the GNU Lesser General Public
-# License as published by the Free Software Foundation; either
-# version 2.1 of the License, or (at your option) any later version.
+# Copyright @ Daniel Lezcano <daniel.lezcano@free.fr>
+# Copyright © 2018 Christian Brauner <christian.brauner@ubuntu.com>
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
+# USA
+
+LXC_MAPPED_UID=
+LXC_MAPPED_GID=
+
+# Make sure the usual locations are in PATH
+export PATH=$PATH:/usr/sbin:/usr/bin:/sbin:/bin
+
+in_userns() {
+ [ -e /proc/self/uid_map ] || { echo no; return; }
+ while read -r line; do
+ fields="$(echo "$line" | awk '{ print $1 " " $2 " " $3 }')"
+ if [ "${fields}" = "0 0 4294967295" ]; then
+ echo no;
+ return;
+ fi
+ if echo "${fields}" | grep -q " 0 1$"; then
+ echo userns-root;
+ return;
+ fi
+ done < /proc/self/uid_map
-# This library is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
-# Lesser General Public License for more details.
+ [ "$(cat /proc/self/uid_map)" = "$(cat /proc/1/uid_map)" ] && { echo userns-root; return; }
+ echo yes
+}
-# You should have received a copy of the GNU Lesser General Public
-# License along with this library; if not, write to the Free Software
-# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+USERNS="$(in_userns)"
install_busybox()
{
- rootfs=$1
- name=$2
- res=0
- tree="\
-$rootfs/selinux \
-$rootfs/dev \
-$rootfs/home \
-$rootfs/root \
-$rootfs/etc \
-$rootfs/etc/init.d \
-$rootfs/bin \
-$rootfs/usr/bin \
-$rootfs/sbin \
-$rootfs/usr/sbin \
-$rootfs/proc \
-$rootfs/mnt \
-$rootfs/tmp \
-$rootfs/var/log \
-$rootfs/usr/share/udhcpc \
-$rootfs/dev/pts \
-$rootfs/dev/shm \
-$rootfs/lib \
-$rootfs/usr/lib \
-$rootfs/lib64 \
-$rootfs/usr/lib64"
-
- mkdir -p $tree || return 1
- chmod 755 $tree || return 1
-
- pushd $rootfs/dev > /dev/null || return 1
-
- # minimal devices needed for busybox
- mknod tty c 5 0 || res=1
- mknod console c 5 1 || res=1
- chmod 666 tty console || res=1
- mknod tty0 c 4 0 || res=1
- mknod tty1 c 4 0 || res=1
- mknod tty5 c 4 0 || res=1
- chmod 666 tty0 || res=1
- mknod ram0 b 1 0 || res=1
- chmod 600 ram0 || res=1
- mknod null c 1 3 || res=1
- chmod 666 null || res=1
- mknod urandom c 1 9 || res=1
- chmod 666 urandom || res=1
-
- popd > /dev/null
-
- # root user defined
- cat <<EOF >> $rootfs/etc/passwd
+ rootfs="${1}"
+ name="${2}"
+ res=0
+ fstree="\
+${rootfs}/selinux \
+${rootfs}/dev \
+${rootfs}/home \
+${rootfs}/root \
+${rootfs}/etc \
+${rootfs}/etc/init.d \
+${rootfs}/bin \
+${rootfs}/usr/bin \
+${rootfs}/sbin \
+${rootfs}/usr/sbin \
+${rootfs}/proc \
+${rootfs}/sys \
+${rootfs}/mnt \
+${rootfs}/tmp \
+${rootfs}/var/log \
+${rootfs}/usr/share/udhcpc \
+${rootfs}/dev/pts \
+${rootfs}/dev/shm \
+${rootfs}/lib \
+${rootfs}/usr/lib \
+${rootfs}/lib64 \
+${rootfs}/usr/lib64"
+
+ # shellcheck disable=SC2086
+ mkdir -p ${fstree} || return 1
+ # shellcheck disable=SC2086
+ chmod 755 ${fstree} || return 1
+
+ # minimal devices needed for busybox
+ if [ "${USERNS}" = "yes" ]; then
+ for dev in tty console tty0 tty1 ram0 null urandom; do
+ echo "lxc.mount.entry = /dev/${dev} dev/${dev} none bind,optional,create=file 0 0" >> "${path}/config"
+ done
+ else
+ mknod -m 666 "${rootfs}/tty" c 5 0 || res=1
+ mknod -m 666 "${rootfs}/console" c 5 1 || res=1
+ mknod -m 666 "${rootfs}/tty0" c 4 0 || res=1
+ mknod -m 666 "${rootfs}/tty1" c 4 0 || res=1
+ mknod -m 666 "${rootfs}/tty5" c 4 0 || res=1
+ mknod -m 600 "${rootfs}/ram0" b 1 0 || res=1
+ mknod -m 666 "${rootfs}/null" c 1 3 || res=1
+ mknod -m 666 "${rootfs}/zero" c 1 5 || res=1
+ mknod -m 666 "${rootfs}/urandom" c 1 9 || res=1
+ fi
+
+ # root user defined
+ cat <<EOF >> "${rootfs}/etc/passwd"
root:x:0:0:root:/root:/bin/sh
EOF
- cat <<EOF >> $rootfs/etc/group
+ cat <<EOF >> "${rootfs}/etc/group"
root:x:0:root
EOF
# mount everything
- cat <<EOF >> $rootfs/etc/init.d/rcS
+ cat <<EOF >> "${rootfs}/etc/init.d/rcS"
#!/bin/sh
/bin/syslogd
/bin/mount -a
/bin/udhcpc
EOF
- # executable
- chmod 744 $rootfs/etc/init.d/rcS || return 1
-
- # mount points
- cat <<EOF >> $rootfs/etc/fstab
-proc /proc proc defaults 0 0
-shm /dev/shm tmpfs defaults 0 0
-EOF
-
- # writable and readable for other
- chmod 644 $rootfs/etc/fstab || return 1
+ # executable
+ chmod 744 "${rootfs}/etc/init.d/rcS" || return 1
- # launch rcS first then make a console available
- # and propose a shell on the tty, the last one is
- # not needed
- cat <<EOF >> $rootfs/etc/inittab
+ # launch rcS first then make a console available
+ # and propose a shell on the tty, the last one is
+ # not needed
+ cat <<EOF >> "${rootfs}/etc/inittab"
::sysinit:/etc/init.d/rcS
tty1::respawn:/bin/getty -L tty1 115200 vt100
console::askfirst:/bin/sh
EOF
- # writable and readable for other
- chmod 644 $rootfs/etc/inittab || return 1
+ # writable and readable for other
+ chmod 644 "${rootfs}/etc/inittab" || return 1
- cat <<EOF >> $rootfs/usr/share/udhcpc/default.script
+ cat <<EOF >> "${rootfs}/usr/share/udhcpc/default.script"
#!/bin/sh
case "\$1" in
- deconfig)
- ip addr flush dev \$interface
- ;;
-
- renew|bound)
- # flush all the routes
- if [ -n "\$router" ]; then
- ip route del default 2> /dev/null
- fi
-
- # check broadcast
- if [ -n "\$broadcast" ]; then
- broadcast="broadcast \$broadcast"
- fi
-
- # add a new ip address
- ip addr add \$ip/\$mask \$broadcast dev \$interface
-
- if [ -n "\$router" ]; then
- ip route add default via \$router dev \$interface
- fi
-
- [ -n "\$domain" ] && echo search \$domain > /etc/resolv.conf
- for i in \$dns ; do
- echo nameserver \$i >> /etc/resolv.conf
- done
- ;;
+ deconfig)
+ ip addr flush dev \$interface
+ ;;
+
+ renew|bound)
+ # flush all the routes
+ if [ -n "\$router" ]; then
+ ip route del default 2> /dev/null
+ fi
+
+ # check broadcast
+ if [ -n "\$broadcast" ]; then
+ broadcast="broadcast \$broadcast"
+ fi
+
+ # add a new ip address
+ ip addr add \$ip/\$mask \$broadcast dev \$interface
+
+ if [ -n "\$router" ]; then
+ ip route add default via \$router dev \$interface
+ fi
+
+ [ -n "\$domain" ] && echo search \$domain > /etc/resolv.conf
+ for i in \$dns ; do
+ echo nameserver \$i >> /etc/resolv.conf
+ done
+ ;;
esac
exit 0
EOF
- chmod 744 $rootfs/usr/share/udhcpc/default.script
+ chmod 744 "${rootfs}/usr/share/udhcpc/default.script"
- return $res
+ return "${res}"
}
configure_busybox()
{
- rootfs=$1
+ rootfs="${1}"
+
+ if ! which busybox > /dev/null 2>&1; then
+ echo "ERROR: Failed to find busybox binary"
+ return 1
+ fi
+
+ # copy busybox in the rootfs
+ if ! cp "$(which busybox)" "${rootfs}/bin"; then
+ echo "ERROR: Failed to copy busybox binary"
+ return 1
+ fi
+
+ # symlink busybox for the commands it supports
+ # it would be nice to just use "chroot $rootfs busybox --install -s /bin"
+ # but that only works right in a chroot with busybox >= 1.19.0
+ (
+ cd "${rootfs}/bin" || return 1
+ ./busybox --list | grep -v busybox | xargs -n1 ln -s busybox
+ )
+
+ # relink /sbin/init
+ ln "${rootfs}/bin/busybox" "${rootfs}/sbin/init"
+
+ # /etc/fstab must exist for "mount -a"
+ touch "${rootfs}/etc/fstab"
+
+ # passwd exec must be setuid
+ chmod +s "${rootfs}/bin/passwd"
+ touch "${rootfs}/etc/shadow"
+
+ return 0
+}
- which busybox >/dev/null 2>&1
+copy_configuration()
+{
+ path="${1}"
+ rootfs="${2}"
+ name="${3}"
+
+grep -q "^lxc.rootfs.path" "${path}/config" 2>/dev/null || echo "lxc.rootfs.path = ${rootfs}" >> "${path}/config"
+cat <<EOF >> "${path}/config"
+lxc.signal.halt = SIGUSR1
+lxc.signal.reboot = SIGTERM
+lxc.uts.name = "${name}"
+lxc.tty.max = 1
+lxc.pty.max = 1
+lxc.cap.drop = sys_module mac_admin mac_override sys_time
- if [ $? -ne 0 ]; then
- echo "busybox executable is not accessible"
- return 1
- fi
+# When using LXC with apparmor, uncomment the next line to run unconfined:
+#lxc.apparmor.profile = unconfined
- file $(which busybox) | grep -q "statically linked"
- if [ $? -ne 0 ]; then
- echo "warning : busybox is not statically linked."
- echo "warning : The template script may not correctly"
- echo "warning : setup the container environment."
- fi
+lxc.mount.auto = cgroup:mixed proc:mixed sys:mixed
+lxc.mount.entry = shm /dev/shm tmpfs defaults 0 0
+EOF
- # copy busybox in the rootfs
- cp $(which busybox) $rootfs/bin
- if [ $? -ne 0 ]; then
- echo "failed to copy busybox in the rootfs"
- return 1
- fi
+ libdirs="\
+ lib \
+ usr/lib \
+ lib64 \
+ usr/lib64"
- # symlink busybox for the commands it supports
- # it would be nice to just use "chroot $rootfs busybox --install -s /bin"
- # but that only works right in a chroot with busybox >= 1.19.0
- pushd $rootfs/bin > /dev/null || return 1
- ./busybox --help | grep 'Currently defined functions:' -A300 | \
- grep -v 'Currently defined functions:' | tr , '\n' | \
- xargs -n1 ln -s busybox
- popd > /dev/null
-
- # relink /sbin/init
- ln $rootfs/bin/busybox $rootfs/sbin/init
-
- # passwd exec must be setuid
- chmod +s $rootfs/bin/passwd
- touch $rootfs/etc/shadow
- echo "setting root passwd to root"
- echo "root:root" | chroot $rootfs chpasswd
-
-
- # add ssh functionality if dropbear package available on host
- which dropbear >/dev/null 2>&1
- if [ $? -eq 0 ]; then
- # copy dropbear binary
- cp $(which dropbear) $rootfs/usr/sbin
- if [ $? -ne 0 ]; then
- echo "Failed to copy dropbear in the rootfs"
- return 1
- fi
-
- # make symlinks to various ssh utilities
- utils="\
- $rootfs/usr/bin/dbclient \
- $rootfs/usr/bin/scp \
- $rootfs/usr/bin/ssh \
- $rootfs/usr/sbin/dropbearkey \
- $rootfs/usr/sbin/dropbearconvert \
- "
- echo $utils | xargs -n1 ln -s /usr/sbin/dropbear
-
- # add necessary config files
- mkdir $rootfs/etc/dropbear
- dropbearkey -t rsa -f $rootfs/etc/dropbear/dropbear_rsa_host_key &> /dev/null
- dropbearkey -t dss -f $rootfs/etc/dropbear/dropbear_dss_host_key &> /dev/null
-
- echo "'dropbear' ssh utility installed"
+ for dir in ${libdirs}; do
+ if [ -d "/${dir}" ] && [ -d "${rootfs}/${dir}" ]; then
+ echo "lxc.mount.entry = /${dir} ${dir} none ro,bind 0 0" >> "${path}/config"
fi
-
- return 0
+ done
+ echo "lxc.mount.entry = /sys/kernel/security sys/kernel/security none ro,bind,optional 0 0" >> "${path}/config"
}
-copy_configuration()
+remap_userns()
{
- path=$1
- rootfs=$2
- name=$3
+ path="${1}"
-grep -q "^lxc.rootfs" $path/config 2>/dev/null || echo "lxc.rootfs = $rootfs" >> $path/config
-cat <<EOF >> $path/config
-lxc.utsname = $name
-lxc.tty = 1
-lxc.pts = 1
+ if [ -n "$LXC_MAPPED_UID" ] && [ "$LXC_MAPPED_UID" != "-1" ]; then
+ chown "${LXC_MAPPED_UID}" "${path}/config" > /dev/null 2>&1
+ chown -R root "${path}/rootfs" > /dev/null 2>&1
+ fi
-# When using LXC with apparmor, uncomment the next line to run unconfined:
-#lxc.aa_profile = unconfined
-EOF
+ if [ -n "$LXC_MAPPED_GID" ] && [ "$LXC_MAPPED_GID" != "-1" ]; then
+ chgrp "${LXC_MAPPED_GID}" "${path}/config" > /dev/null 2>&1
+ chgrp -R root "${path}/rootfs" > /dev/null 2>&1
+ fi
+}
-if [ -d "$rootfs/lib" ]; then
-cat <<EOF >> $path/config
-lxc.mount.entry = /lib $rootfs/lib none ro,bind 0 0
-lxc.mount.entry = /usr/lib $rootfs/usr/lib none ro,bind 0 0
-EOF
-fi
+usage() {
+ cat <<EOF
+LXC busybox image builder
- libdirs="\
- lib \
- usr/lib \
- lib64 \
- usr/lib64"
+Special arguments:
+[ -h | --help ]: Print this help message and exit.
- for dir in $libdirs; do
- if [ -d "/$dir" ] && [ -d "$rootfs/$dir" ]; then
- echo "lxc.mount.entry = /$dir $dir none ro,bind 0 0" >> $path/config
- fi
- done
-}
-
-usage()
-{
- cat <<EOF
-$1 -h|--help -p|--path=<path>
+LXC internal arguments (do not pass manually!):
+[ --name <name> ]: The container name
+[ --path <path> ]: The path to the container
+[ --rootfs <rootfs> ]: The path to the container's rootfs
+[ --mapped-uid <map> ]: A uid map (user namespaces)
+[ --mapped-gid <map> ]: A gid map (user namespaces)
EOF
- return 0
+ return 0
}
-options=$(getopt -o hp:n: -l help,path:,name: -- "$@")
-if [ $? -ne 0 ]; then
- usage $(basename $0)
- exit 1
+if ! options=$(getopt -o hp:n: -l help,rootfs:,path:,name:,mapped-uid:,mapped-gid: -- "$@"); then
+ usage
+ exit 1
fi
eval set -- "$options"
while true
do
- case "$1" in
- -h|--help) usage $0 && exit 0;;
- -p|--path) path=$2; shift 2;;
- -n|--name) name=$2; shift 2;;
- --) shift 1; break ;;
- *) break ;;
- esac
+ case "$1" in
+ -h|--help) usage && exit 1;;
+ -n|--name) name=$2; shift 2;;
+ -p|--path) path=$2; shift 2;;
+ --rootfs) rootfs=$2; shift 2;;
+ --mapped-uid) LXC_MAPPED_UID=$2; shift 2;;
+ --mapped-gid) LXC_MAPPED_GID=$2; shift 2;;
+ --) shift 1; break ;;
+ *) break ;;
+ esac
done
-if [ "$(id -u)" != "0" ]; then
- echo "This script should be run as 'root'"
- exit 1
-fi
-
-if [ -z "$path" ]; then
- echo "'path' parameter is required"
+# Check that we have all variables we need
+if [ -z "${name}" ] || [ -z "${path}" ] || [ -z "${rootfs}" ]; then
+ echo "ERROR: Please pass the name, path, and rootfs for the container" 1>&2
exit 1
fi
# detect rootfs
config="$path/config"
-if grep -q '^lxc.rootfs' $config 2>/dev/null ; then
- rootfs=`grep 'lxc.rootfs =' $config | awk -F= '{ print $2 }'`
-else
- rootfs=$path/rootfs
+if [ -z "$rootfs" ]; then
+ if grep -q '^lxc.rootfs.path' "${config}" 2> /dev/null ; then
+ rootfs=$(awk -F= '/^lxc.rootfs.path =/{ print $2 }' "${config}")
+ else
+ rootfs="${path}/rootfs"
+ fi
fi
-install_busybox $rootfs $name
-if [ $? -ne 0 ]; then
- echo "failed to install busybox's rootfs"
- exit 1
+if ! install_busybox "${rootfs}" "${name}"; then
+ echo "ERROR: Failed to install rootfs"
+ exit 1
fi
-configure_busybox $rootfs
-if [ $? -ne 0 ]; then
- echo "failed to configure busybox template"
- exit 1
+if ! configure_busybox "${rootfs}"; then
+ echo "ERROR: Failed to configure busybox"
+ exit 1
fi
-copy_configuration $path $rootfs $name
-if [ $? -ne 0 ]; then
- echo "failed to write configuration file"
- exit 1
+if ! copy_configuration "${path}" "${rootfs}" "${name}"; then
+ echo "ERROR: Failed to write config file"
+ exit 1
+fi
+
+if ! remap_userns "${path}"; then
+ echo "ERROR: Failed to change idmappings"
+ exit 1
fi