-
-
Save smoser/2e622c67e8a679630d6e70e63b0f32d0 to your computer and use it in GitHub Desktop.
| #!/bin/bash | |
| # Create application containers from OCI images | |
| # Copyright © 2014 Stéphane Graber <[email protected]> | |
| # Copyright © 2017 Serge Hallyn <[email protected]> | |
| # | |
| # 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 | |
| set -eu | |
| # Make sure the usual locations are in PATH | |
| export PATH="$PATH:/usr/sbin:/usr/bin:/sbin:/bin" | |
| # Check for required binaries | |
| for bin in skopeo umoci jq; do | |
| if ! command -v $bin >/dev/null 2>&1; then | |
| echo "ERROR: Missing required tool: $bin" 1>&2 | |
| exit 1 | |
| fi | |
| done | |
| LXC_OCI_MOUNT="lxc-oci-mount" | |
| ATOMFS="atomfs" | |
| LOCALSTATEDIR=/var | |
| LXC_TEMPLATE_CONFIG=/usr/share/lxc/config | |
| LXC_HOOK_DIR=/usr/share/lxc/hooks | |
| OCI_MOUNT="" | |
| # Some useful functions | |
| cleanup() { | |
| if [ -d "${LXC_ROOTFS}.tmp" ]; then | |
| rm -Rf "${LXC_ROOTFS}.tmp" | |
| fi | |
| if [ -n "$OCI_MOUNT" ]; then | |
| echo "atomfs unmount $OCI_MOUNT" >&2 | |
| "${ATOMFS}" umount "$OCI_MOUNT" | |
| OCI_MOUNT="" | |
| fi | |
| } | |
| 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 | |
| if [ -e /proc/1/uid_map ]; then | |
| if [ "$(cat /proc/self/uid_map)" = "$(cat /proc/1/uid_map)" ]; then | |
| echo userns-root | |
| return | |
| fi | |
| fi | |
| echo yes | |
| } | |
| getmanifestpath() { | |
| local basedir="$1" ref="$2" p="" | |
| # if given 'sha256:<hash>' then return the blobs/sha256/hash | |
| case "$ref" in | |
| sha256:*) | |
| p="$basedir/blobs/sha256/${ref#sha256:}" | |
| [ -f "$p" ] && echo "$p" && return 0 | |
| echo "could not find manifest path to blob $ref. file did not exist: $p" >&2 | |
| return 1 | |
| esac | |
| # find the reference by annotation | |
| local blobref="" hashtype="" hashval="" | |
| blobref=$(jq -c -r --arg q "$ref" '.manifests[] | if .annotations."org.opencontainers.image.ref.name" == $q then .digest else empty end' < "${basedir}/index.json") | |
| # blobref is 'hashtype:hash' | |
| hashtype="${blobref%%:*}" | |
| hashval="${blobref#*:}" | |
| p="$basedir/blobs/$hashtype/$hashval" | |
| [ -f "$p" ] && echo "$p" && return 0 | |
| echo "did not find manifest for $ref. file did not exist: $p" >&2 | |
| return 1 | |
| } | |
| getconfigpath() { | |
| local basedir="$1" mfpath="$2" cdigest="" | |
| # Ok we have the image config digest, now get the config ref from the manifest. | |
| # shellcheck disable=SC2039 | |
| cdigest=$(jq -c -r '.config.digest' < "$mfpath") | |
| if [ -z "${cdigest}" ]; then | |
| echo "container config not found" >&2 | |
| return | |
| fi | |
| # cdigest is 'hashtype:hash' | |
| local ht="${cdigest%%:*}" hv="${cdigest#*:}" p="" | |
| p="$basedir/blobs/$ht/$hv" | |
| if [ ! -f "$p" ]; then | |
| echo "config file did not exist for digest $cdigest" >&2 | |
| return 1 | |
| fi | |
| echo "$p" | |
| } | |
| getlayermediatype() { | |
| jq -c -r '.layers[0].mediaType' <"$1" | |
| } | |
| # Get entrypoint from oci image. Use sh if unspecified | |
| getep() { | |
| if [ "$#" -eq 0 ]; then | |
| echo "/bin/sh" | |
| return | |
| fi | |
| configpath="$1" | |
| ep=$(jq -c '.config.Entrypoint[]?'< "${configpath}" | tr '\n' ' ') | |
| cmd=$(jq -c '.config.Cmd[]?'< "${configpath}" | tr '\n' ' ') | |
| if [ -z "${ep}" ]; then | |
| ep="${cmd}" | |
| if [ -z "${ep}" ]; then | |
| ep="/bin/sh" | |
| fi | |
| elif [ -n "${cmd}" ]; then | |
| ep="${ep} ${cmd}" | |
| fi | |
| echo "${ep}" | |
| return | |
| } | |
| # get environment from oci image. | |
| getenv() { | |
| if [ "$#" -eq 0 ]; then | |
| return | |
| fi | |
| configpath="$1" | |
| env=$(jq -c -r '.config.Env[]'< "${configpath}") | |
| echo "${env}" | |
| return | |
| } | |
| # check var is decimal. | |
| isdecimal() { | |
| var="$1" | |
| if [ "${var}" -eq "${var}" ] 2> /dev/null; then | |
| return 0 | |
| else | |
| return 1 | |
| fi | |
| } | |
| # get uid, gid from oci image. | |
| getuidgid() { | |
| configpath="$1" | |
| rootpath="$2" | |
| passwdpath="${rootpath}/etc/passwd" | |
| grouppath="${rootpath}/etc/group" | |
| usergroup=$(jq -c -r '.config.User' < "${configpath}") | |
| # shellcheck disable=SC2039 | |
| usergroup=(${usergroup//:/ }) | |
| user=${usergroup[0]:-0} | |
| if ! isdecimal "${user}"; then | |
| if [ -f ${passwdpath} ]; then | |
| user=$(grep "^${user}:" "${passwdpath}" | awk -F: '{print $3}') | |
| else | |
| user=0 | |
| fi | |
| fi | |
| group=${usergroup[1]:-} | |
| if [ -z "${group}" ]; then | |
| if [ -f "${passwdpath}" ]; then | |
| group=$(grep "^[^:]*:[^:]*:${user}:" "${passwdpath}" | awk -F: '{print $4}') | |
| else | |
| group=0 | |
| fi | |
| elif ! isdecimal "${group}"; then | |
| if [ -f "${grouppath}" ]; then | |
| group=$(grep "^${group}:" "${grouppath}" | awk -F: '{print $3}') | |
| else | |
| group=0 | |
| fi | |
| fi | |
| echo "${user:-0} ${group:-0}" | |
| return | |
| } | |
| # get cwd from oci image. | |
| getcwd() { | |
| if [ "$#" -eq 0 ]; then | |
| echo "/" | |
| return | |
| fi | |
| configpath="$1" | |
| cwd=$(jq -c -r '.config.WorkingDir // "/"' < "${configpath}") | |
| echo "${cwd}" | |
| return | |
| } | |
| usage() { | |
| cat <<EOF | |
| LXC container template for OCI images | |
| Special arguments: | |
| [ -h | --help ]: Print this help message and exit. | |
| Required arguments: | |
| [ -u | --url <url> ]: The OCI image URL | |
| Optional arguments: | |
| [ --username <username> ]: The username for the registry | |
| [ --password <password> ]: The password for the registry | |
| 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 | |
| } | |
| echo "got $#: $*" | |
| if ! options=$(getopt -o u:h -l help,url:,username:,password:,no-cache,dhcp,name:,path:,rootfs:,mapped-uid:,mapped-gid: -- "$@"); then | |
| usage | |
| exit 1 | |
| fi | |
| eval set -- "$options" | |
| OCI_URL="" | |
| OCI_USERNAME= | |
| OCI_PASSWORD= | |
| OCI_USE_CACHE="true" | |
| OCI_USE_DHCP="false" | |
| LXC_MAPPED_GID= | |
| LXC_MAPPED_UID= | |
| LXC_NAME= | |
| LXC_PATH= | |
| LXC_ROOTFS= | |
| while :; do | |
| case "$1" in | |
| -h|--help) usage && exit 1;; | |
| -u|--url) OCI_URL=$2; shift 2;; | |
| --username) OCI_USERNAME=$2; shift 2;; | |
| --password) OCI_PASSWORD=$2; shift 2;; | |
| --no-cache) OCI_USE_CACHE="false"; shift 1;; | |
| --dhcp) OCI_USE_DHCP="true"; shift 1;; | |
| --name) LXC_NAME=$2; shift 2;; | |
| --path) LXC_PATH=$2; shift 2;; | |
| --rootfs) LXC_ROOTFS=$2; shift 2;; | |
| --mapped-uid) LXC_MAPPED_UID=$2; shift 2;; | |
| --mapped-gid) LXC_MAPPED_GID=$2; shift 2;; | |
| *) break;; | |
| esac | |
| done | |
| # Check that we have all variables we need | |
| if [ -z "$LXC_NAME" ] || [ -z "$LXC_PATH" ] || [ -z "$LXC_ROOTFS" ]; then | |
| echo "ERROR: Not running through LXC" 1>&2 | |
| exit 1 | |
| fi | |
| if [ -z "$OCI_URL" ]; then | |
| echo "ERROR: no OCI URL given" | |
| exit 1 | |
| fi | |
| if [ -n "$OCI_PASSWORD" ] && [ -z "$OCI_USERNAME" ]; then | |
| echo "ERROR: password given but no username specified" | |
| exit 1 | |
| fi | |
| if [ "${OCI_USE_CACHE}" = "true" ]; then | |
| if ! skopeo copy --help | grep -q 'dest-shared-blob-dir'; then | |
| echo "INFO: skopeo doesn't support blob caching" | |
| OCI_USE_CACHE="false" | |
| fi | |
| fi | |
| USERNS=$(in_userns) | |
| if [ "$USERNS" = "yes" ]; then | |
| if [ -z "$LXC_MAPPED_UID" ] || [ "$LXC_MAPPED_UID" = "-1" ]; then | |
| echo "ERROR: In a user namespace without a map" 1>&2 | |
| exit 1 | |
| fi | |
| fi | |
| OCI_DIR="$LXC_PATH/oci" | |
| if [ "${OCI_USE_CACHE}" = "true" ]; then | |
| if [ "$USERNS" = "yes" ]; then | |
| DOWNLOAD_BASE="${HOME}/.cache/lxc" | |
| else | |
| DOWNLOAD_BASE="${LOCALSTATEDIR}/cache/lxc" | |
| fi | |
| else | |
| DOWNLOAD_BASE="$OCI_DIR" | |
| fi | |
| mkdir -p "${DOWNLOAD_BASE}" | |
| # Trap all exit signals | |
| trap cleanup EXIT HUP INT TERM | |
| # Download the image | |
| # shellcheck disable=SC2039 | |
| skopeo_args=("--remove-signatures" --insecure-policy) | |
| if [ -n "$OCI_USERNAME" ]; then | |
| CREDENTIALS="${OCI_USERNAME}" | |
| if [ -n "$OCI_PASSWORD" ]; then | |
| CREDENTIALS="${CREDENTIALS}:${OCI_PASSWORD}" | |
| fi | |
| # shellcheck disable=SC2039 | |
| skopeo_args+=(--src-creds "${CREDENTIALS}") | |
| fi | |
| OCI_NAME="$LXC_NAME" | |
| if [ "${OCI_USE_CACHE}" = "true" ]; then | |
| skopeo_args+=(--dest-shared-blob-dir "${DOWNLOAD_BASE}") | |
| mkdir -p "${OCI_DIR}/blobs/" | |
| ln -s "${DOWNLOAD_BASE}/sha256" "${OCI_DIR}/blobs/sha256" | |
| fi | |
| skopeo copy "${skopeo_args[@]}" "${OCI_URL}" "oci:${OCI_DIR}:${OCI_NAME}" | |
| mfpath=$(getmanifestpath "${OCI_DIR}" "${OCI_NAME}") | |
| OCI_CONF_FILE=$(getconfigpath "${OCI_DIR}" "$mfpath") | |
| mediatype=$(getlayermediatype "$mfpath") | |
| echo "mfpath=$mfpath conf=$OCI_CONF_FILE" 1>&2 | |
| echo "mediatype=$mediatype" | |
| echo "DOWNLOAD_BASE=$DOWNLOAD_BASE" >&2 | |
| echo "OCI_DIR=$OCI_DIR" >&2 | |
| case "$mediatype" in | |
| #application/vnd.oci.image.layer.v1.tar+gzip | |
| application/vnd.oci.image.layer.v1.tar*) | |
| echo "Unpacking tar rootfs" 2>&1 | |
| # shellcheck disable=SC2039 | |
| umoci_args=("") | |
| if [ -n "$LXC_MAPPED_UID" ] && [ "$LXC_MAPPED_UID" != "-1" ]; then | |
| # shellcheck disable=SC2039 | |
| umoci_args+=(--rootless) | |
| fi | |
| # shellcheck disable=SC2039 | |
| # shellcheck disable=SC2068 | |
| umoci --log=error unpack ${umoci_args[@]} --image "${OCI_DIR}:${OCI_NAME}" "${LXC_ROOTFS}.tmp" | |
| find "${LXC_ROOTFS}.tmp/rootfs" -mindepth 1 -maxdepth 1 -exec mv '{}' "${LXC_ROOTFS}/" \; | |
| ;; | |
| #application/vnd.stacker.image.layer.squashfs+zstd+verity | |
| application/vnd.*.image.layer.squashfs*) | |
| echo "squashfs type $mediatype" >&2 | |
| echo ${ATOMFS} mount "${OCI_DIR}:${OCI_NAME}" "$LXC_ROOTFS" >&2 | |
| ${ATOMFS} mount "${OCI_DIR}:${OCI_NAME}" "$LXC_ROOTFS" | |
| OCI_MOUNT="$LXC_ROOTFS" | |
| ;; | |
| *) | |
| echo "Unknown media type $mediatype" >&2 | |
| exit 1 | |
| ;; | |
| esac | |
| LXC_CONF_FILE="${LXC_PATH}/config" | |
| entrypoint=$(getep "${OCI_CONF_FILE}") | |
| echo "lxc.execute.cmd = '${entrypoint}'" >> "${LXC_CONF_FILE}" | |
| echo "lxc.mount.auto = proc:mixed sys:mixed cgroup:mixed" >> "${LXC_CONF_FILE}" | |
| case "$mediatype" in | |
| application/vnd.*.image.layer.squashfs*) | |
| echo "lxc.hook.pre-mount = ${LXC_OCI_MOUNT}" >> "${LXC_CONF_FILE}";; | |
| esac | |
| environment=$(getenv "${OCI_CONF_FILE}") | |
| # shellcheck disable=SC2039 | |
| while read -r line; do | |
| echo "lxc.environment = ${line}" >> "${LXC_CONF_FILE}" | |
| done <<< "${environment}" | |
| if [ -e "${LXC_TEMPLATE_CONFIG}/common.conf" ]; then | |
| echo "lxc.include = ${LXC_TEMPLATE_CONFIG}/common.conf" >> "${LXC_CONF_FILE}" | |
| fi | |
| if [ -n "$LXC_MAPPED_UID" ] && [ "$LXC_MAPPED_UID" != "-1" ] && [ -e "${LXC_TEMPLATE_CONFIG}/userns.conf" ]; then | |
| echo "lxc.include = ${LXC_TEMPLATE_CONFIG}/userns.conf" >> "${LXC_CONF_FILE}" | |
| fi | |
| if [ -e "${LXC_TEMPLATE_CONFIG}/oci.common.conf" ]; then | |
| echo "lxc.include = ${LXC_TEMPLATE_CONFIG}/oci.common.conf" >> "${LXC_CONF_FILE}" | |
| fi | |
| if [ "${OCI_USE_DHCP}" = "true" ]; then | |
| echo "lxc.hook.start-host = ${LXC_HOOK_DIR}/dhclient" >> "${LXC_CONF_FILE}" | |
| echo "lxc.hook.stop = ${LXC_HOOK_DIR}/dhclient" >> "${LXC_CONF_FILE}" | |
| fi | |
| echo "lxc.uts.name = ${LXC_NAME}" >> "${LXC_CONF_FILE}" | |
| # set the hostname | |
| cat <<EOF > "${LXC_ROOTFS}/etc/hostname" | |
| ${LXC_NAME} | |
| EOF | |
| # set minimal hosts | |
| cat <<EOF > "${LXC_ROOTFS}/etc/hosts" | |
| 127.0.0.1 localhost | |
| 127.0.1.1 ${LXC_NAME} | |
| ::1 localhost ip6-localhost ip6-loopback | |
| fe00::0 ip6-localnet | |
| ff00::0 ip6-mcastprefix | |
| ff02::1 ip6-allnodes | |
| ff02::2 ip6-allrouters | |
| EOF | |
| # shellcheck disable=SC2039 | |
| uidgid=($(getuidgid "${OCI_CONF_FILE}" "${LXC_ROOTFS}" )) | |
| # shellcheck disable=SC2039 | |
| echo "lxc.init.uid = ${uidgid[0]}" >> "${LXC_CONF_FILE}" | |
| # shellcheck disable=SC2039 | |
| echo "lxc.init.gid = ${uidgid[1]}" >> "${LXC_CONF_FILE}" | |
| cwd=$(getcwd "${OCI_CONF_FILE}") | |
| echo "lxc.init.cwd = ${cwd}" >> "${LXC_CONF_FILE}" | |
| if [ -n "$LXC_MAPPED_UID" ] && [ "$LXC_MAPPED_UID" != "-1" ]; then | |
| chown "$LXC_MAPPED_UID" "$LXC_PATH/config" "$LXC_PATH/fstab" > /dev/null 2>&1 || true | |
| fi | |
| if [ -n "$LXC_MAPPED_GID" ] && [ "$LXC_MAPPED_GID" != "-1" ]; then | |
| chgrp "$LXC_MAPPED_GID" "$LXC_PATH/config" "$LXC_PATH/fstab" > /dev/null 2>&1 || true | |
| fi | |
| exit 0 |
| #!/bin/sh | |
| show() { | |
| local i=0 | |
| echo "$1" | |
| shift | |
| while [ $# -gt 0 ]; do | |
| printf "%02d %s\n" "$i" "$1" | |
| shift | |
| i=$((i+1)) | |
| done | |
| echo "== env ==" | |
| env | grep LXC | |
| return 0 | |
| } | |
| fail(){ echo "$@" 1>&2; exit 1; } | |
| #exec >>/tmp/my.log 2>&1 | |
| #show "$0" "$@" | |
| #set -x | |
| oci="${LXC_ROOTFS_PATH%/rootfs}/oci" | |
| atomfs mount "$oci:${LXC_NAME}" "${LXC_ROOTFS_PATH}" || | |
| fail "mount failed" |
| * Plan is to run a lxd vm of jammy. | |
| * Inside install lxc-utils, skopeo, puzzleos ppa, mdp | |
| * Run a local zot | |
| https://gist.github.com/smoser/5a3623e497f90925ff4d9344a85c50a3 | |
| * install python3-pygments (pygmentize) | |
| $ skopeo copy --src-tls-verify=false --insecure-policy | |
| docker://atom-lab-4:5000/docker-sync/alpine:edge oci:./oci-repo:alpine:edge | |
| $ stacker build \ | |
| --substitute=DOCKER_BASE_INSECURE=true \ | |
| --substitute=DOCKER_BASE=docker://atom-lab-4:5000/docker-sync/ stacker.yaml | |
| TODO: | |
| * read for verity info https://www.starlab.io/blog/dm-verity-in-embedded-device-security | |
| * ociv2 https://hackmd.io/@cyphar/ociv2-brainstorm | |
| * ociv2 https://www.cyphar.com/blog/post/20190121-ociv2-images-i-tar | |
| * Need a picture | |
| * overall change tar -> squash | |
| * https://asciiflow.com/#/ | |
| https://github.com/lewish/asciiflow | |
| * need an lxc template script | |
| * notary or cosign | |
| https://docs.sigstore.dev/cosign/overview/ | |
| https://github.com/notaryproject/notaryproject | |
| cosign by ram: https://aci-github.cisco.com/gist/rchincha/cdf87155fab5365b234f8c7ae3df0cd2#file-sign-stacker-yaml-L62 | |
| mkdir squashfs-oci | |
| cd squashfs-oci | |
| cp ../stacker.yaml . | |
| show stacker.yaml | |
| ## FIXME: in vm, have cert registered, stacker.yaml point a localhost | |
| $ stacker build \ | |
| --substitute=DOCKER_BASE_INSECURE=true \ | |
| --substitute=DOCKER_BASE=docker://atom-lab-4:5000/docker-sync/ stacker.yaml | |
| show oci/index.json | |
| show <sha> | |
| # mediaType | |
| $ lxc-create --name=bbdemo --template=atomfs oci:busybox-custom | |
| $ lxc-execute ---name=bbdemo cat /proc/mounts | |
| root | |
| $ sudo PATH=$PWD/bin:$PATH lxc-create -nfooroot -t$PWD/lxc-oci -- | |
| --url=docker://localhost:5000/smoser/talkrootfs-squashfs:latest | |
| non-root | |
| Both have manifest type | |
| application/vnd.oci.image.manifest.v1+json | |
| - application/vnd.oci.image.layer.v1.tar+gzip | |
| + application/vnd.stacker.image.layer.squashfs+zstd+verity | |
| { | |
| "default": [{"type": "reject"}], | |
| "transports": { | |
| "oci": {"": [{"type": "insecureAcceptAnything"}]}, | |
| "docker": { | |
| "docker.io/library": [{"type": "insecureAcceptAnything"}], | |
| "localhost:5000": [ | |
| { | |
| "keyPath": "/home/smoser/cosign.pub", | |
| "signedIdentity": { | |
| "type": "matchRepository" | |
| }, | |
| "type": "sigstoreSigned" | |
| } | |
| ] | |
| } | |
| } | |
| } |
| # this is /etc/containers/registries.d/default.yaml | |
| #default-docker: | |
| # use-sigstore-attachments: true | |
| docker: | |
| localhost:5000: | |
| use-sigstore-attachments: true | |
| #!/bin/sh | |
| # shellcheck disable=SC3043 | |
| # start in a lxd vm launched like this: | |
| DEFUSER="chicken1" | |
| PUBKEY="ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICE48uGW69AleaoUtgaJRYffXWAajmQAlCbF6TRJquSm smoser@crabapple" | |
| stderr() { echo "$@" 1>&2; } | |
| run() { | |
| local rc="" | |
| stderr "execute:" "$@" | |
| "$@" | |
| rc="$?" | |
| [ $rc -eq 0 ] || stderr "failed: $rc" | |
| return $rc | |
| } | |
| startvm() { | |
| local name="${1:-fosdemo}" | |
| run lxc init --vm images:ubuntu/22.04/cloud "$name" && | |
| run lxc config set "$name" limits.memory 4096MB && | |
| run lxc start "$name" | |
| return | |
| } | |
| getbin(){ | |
| local name="$1" url="$2" f="" | |
| f="/usr/local/bin/$name" | |
| if [ -f "$f" ]; then | |
| stderr "already have $f" | |
| return 0 | |
| fi | |
| wget -O "$f.tmp" "$url" || return | |
| case "$url" in | |
| *.gz) zcat "$f.tmp" > "$f" || { rm -f "$f"; return 1; };; | |
| *) mv "$f.tmp" "$f" || { rm -f "$f.tmp" ; return 1; };; | |
| esac | |
| chmod 755 "$f" | |
| } | |
| setupvm() { | |
| local user="$1" | |
| local f="/etc/sysctl.d/99-dmesg.conf" | |
| run echo "kernel.dmesg_restrict = 0" > "$f" || return | |
| run add-apt-repository -y ppa:puzzleos/dev || return | |
| run apt-get update || return | |
| run apt-get install --no-install-recommends --assume-yes lxc-utils \ | |
| lxc-templates screen python3-pygments squashfuse umoci libsquashfs1 \ | |
| wget ca-certificates git apache2-utils libgpgme11 uidmap openssh-server \ | |
| squashfs-tools file jq \ | |
| apparmor cryptsetup lxcfs \ | |
| libpam-cgfs dbus-user-session \ | |
| || return | |
| [ -f /etc/fuse.conf ] || { stderr "no /etc/fuse.conf"; return 1; } | |
| if ! grep -q ^user_allow_other$ /etc/fuse.conf; then | |
| echo "user_allow_other" >> /etc/fuse.conf | |
| fi | |
| getbin zot https://github.com/project-zot/zot/releases/download/v1.4.3/zot-linux-amd64 && | |
| getbin cosign https://github.com/sigstore/cosign/releases/download/v2.0.0-rc.1/cosign-linux-amd64 && | |
| getbin stacker https://github.com/project-stacker/stacker/releases/download/v1.0.0-rc2/stacker && | |
| getbin zli https://github.com/project-zot/zot/releases/download/v1.4.3/zli-linux-amd64 && | |
| getbin skopeo http://smoser.brickies.net/fosdemo/skopeo.gz && | |
| getbin atomfs http://smoser.brickies.net/fosdemo/atomfs.gz && | |
| : || return | |
| } | |
| doadduser() { | |
| local user="${1:-$DEFUSER}" | |
| run adduser --disabled-password "--gecos=$user" "$user" | |
| run sh -c \ | |
| 'umask 226 && echo "$1 ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/$1-user' -- "$user" | |
| } | |
| gitclone() { | |
| local url="$1" dir="$2" | |
| if [ -d "$dir/.git" ]; then | |
| stderr "$dir already cloned" | |
| ( cd "$dir" && git fetch ) | |
| return | |
| fi | |
| run git clone "$url" "$dir" | |
| } | |
| # run gitclone https://gist.github.com/2e622c67e8a679630d6e70e63b0f32d0.git talk && | |
| zotsetup() { | |
| run gitclone https://gist.github.com/5a3623e497f90925ff4d9344a85c50a3.git local-zot && | |
| : || return 1 | |
| ( cd local-zot && run ./generate-certs certs && mkdir logs ) | |
| ( cd local-zot && run htpasswd -D htpasswd.txt zot && | |
| run htpasswd -bB htpasswd.txt zot zot ) || return | |
| mkdir -p .docker && pw=$(printf "%s" zot:zot | base64) && | |
| cat > .docker/config.json <<EOF | |
| { | |
| "auths": { | |
| "localhost:5000": {"auth": "$pw"} | |
| } | |
| } | |
| EOF | |
| ( cd local-zot && | |
| run sudo cp certs/ca.pem /usr/local/share/ca-certificates/localhost-zot.crt && | |
| run sudo update-ca-certificates | |
| ) || return | |
| run cp ~/talk/zot-sync local-zot/zot-sync && | |
| run cp ~/talk/zot-start local-zot/zot-start && | |
| run ./local-zot/zot-start || return | |
| run zli config add localhost https://127.0.0.1:5000/ | |
| } | |
| zotsync() { | |
| cd ~/local-zot/ && run ./zot-sync | |
| } | |
| usersetup() { | |
| run gitclone https://gist.github.com/2e622c67e8a679630d6e70e63b0f32d0.git talk && | |
| : || return | |
| [ -d .ssh ] || run mkdir --mode 700 .ssh || return | |
| if [ ! -f .ssh/authorized_keys ]; then | |
| : > .ssh/authorized_keys && chmod 600 .ssh/authorized_keys || return | |
| fi | |
| grep -q "$PUBKEY" .ssh/authorized_keys || | |
| printf "%s\n" "$PUBKEY" >>.ssh/authorized_keys || | |
| return | |
| ( cd talk && | |
| sudo mkdir -p /etc/containers/registries.d && | |
| run sudo cp -v registry-default.yaml /etc/containers/registries.d/default.yaml && | |
| run sudo cp -v policy.json /etc/containers/ | |
| ) || return | |
| local d="/usr/share/lxc/templates" tmpl="" | |
| tmpl="$d/lxc-oci" | |
| if [ -f "$tmpl" ] && [ ! -f "$tmpl.dist" ]; then | |
| run sudo cp -v "$tmpl" "$tmpl.dist" || return | |
| fi | |
| ( cd talk && | |
| run sudo cp lxc-oci "$tmpl" && | |
| run sudo chmod 755 "$tmpl" && | |
| run sudo cp show lxc-oci-mount /usr/local/bin && | |
| run sudo chmod ugo+x /usr/local/bin/* ) || | |
| return | |
| cat >.screenrc <<EOF | |
| hardstatus alwayslastline "%-Lw%{= BW}%50>%n%f* %t%{-}%+Lw%< %= %H" | |
| defscrollback 8096 | |
| EOF | |
| zotsetup || return | |
| zotsync || return | |
| lxcsetup || return | |
| sudo stacker unpriv-setup | |
| } | |
| lxcsetup() { | |
| local user=${1:-${DEFUSER}} | |
| mkdir -p .config/lxc | |
| cat > .config/lxc/default.conf <<EOF | |
| lxc.include = /etc/lxc/default.conf | |
| lxc.idmap = u 0 165536 65536 | |
| lxc.idmap = g 0 165536 65536 | |
| EOF | |
| [ $? -eq 0 ] || return | |
| local lpath="/lxc/$user" | |
| cat > .config/lxc/lxc.conf <<EOF | |
| lxc.lxcpath = $lpath | |
| EOF | |
| [ $? -eq 0 ] || return | |
| run sudo mkdir -p "$lpath" && | |
| run sudo chown "$user:$user" "$lpath" || | |
| return | |
| local f=/etc/lxc/lxc-usernet | |
| if [ -f "$f" ]; then | |
| sudo touch "$f" | |
| fi | |
| if ! grep -q "$user" "$f"; then | |
| echo "$user veth lxcbr0 32" | run sudo tee "$f" | |
| fi | |
| # You need to edit | |
| # /etc/apparmor.d/abstractions/lxc/start-container | |
| local aaprof="/etc/apparmor.d/abstractions/lxc/start-container" | |
| if [ ! -f "$aaprof" ]; then | |
| echo no "$aaprof file" | |
| return 1 | |
| fi | |
| if ! grep -q 'fstype=fuse[*]' "$aaprof"; then | |
| if [ ! -f "$aaprof.dist" ]; then | |
| run cp "$aaprof" "$aaprof.dist" | |
| fi | |
| cat >>"$aaprof" <<EOF | |
| # add 'mount fstype=fuse*' | |
| # without this here (and with the '*'), we get: | |
| # dmesg errors like fstype="fuse.squashfuse_ll" srcname="squashfuse_ll" | |
| mount fstype=fuse*, | |
| EOF | |
| [ $? -eq 0 ] || return | |
| fi | |
| # FIXME: you also may need to add 'fstype=fuse' to | |
| # /etc/apparmor.d/lxc/lxc-default-cgns | |
| } | |
| case "$1" in | |
| startvm|setupvm|doadduser|usersetup) | |
| n="$1" | |
| shift | |
| $n "$@" | |
| exit;; | |
| main) | |
| setupvm && doadduser && | |
| sudo -Hu "$DEFUSER" --login -- "$0" usersetup | |
| ;; | |
| *) stderr "unknown command $1"; exit 1;; | |
| esac |
| #!/bin/sh | |
| # shellcheck disable=SC2166 | |
| Usage() { | |
| cat <<EOF | |
| ${0##*/} [type] file | |
| show file of type 'type'. if type not given, then try to figure out. | |
| EOF | |
| } | |
| fail(){ echo "$@" 1>&2; exit 1; } | |
| [ $# -eq 1 -o $# -eq 2 ] || { Usage 1>&2; exit 1; } | |
| [ "$1" = "-h" -o "$1" = "--help" ] && { Usage; exit 0; } | |
| [ $# -eq 2 ] && ftype="$1" && shift | |
| file="$1" | |
| if [ ! -f "$file" ]; then | |
| cand=$(echo "$file" | sed 's,sha256:,sha256/,') | |
| if [ -f "$cand" ]; then | |
| file="$cand" | |
| elif [ -f "oci/blobs/$cand" ]; then | |
| file="oci/blobs/$cand" | |
| else | |
| fail "$file: not a file" | |
| fi | |
| fi | |
| decomp="" | |
| if [ -z "$ftype" -o "$ftype" = "auto" ]; then | |
| fout=$(file --uncompress --dereference "$file") || | |
| fail "failed to get type of $file" | |
| case "$fout" in | |
| *gzip\ compressed*) | |
| decomp="zcat";; | |
| esac | |
| fmt="cat" | |
| ftype="" | |
| case "$fout" in | |
| *JSON*) | |
| fmt="python3 -m json.tool" | |
| ftype="json";; | |
| *tar\ archive*) | |
| fmt="tar -tvf -" | |
| ftype="fstar";; | |
| *shell\ script*) | |
| ftype="shell";; | |
| *) | |
| case "$file" in | |
| *.yaml) ftype="yaml";; | |
| esac | |
| esac | |
| fi | |
| [ -n "$ftype" ] && targ="-l$ftype" || targ="-g" | |
| set -- pygmentize "$targ" -O style=solarized-dark,linenos=1 | |
| echo "$file" | |
| if [ -n "$decomp" ]; then | |
| echo "fmt=$fmt" | |
| $decomp "$file" | $fmt | "$@" | |
| elif [ "$fmt" = "cat" ]; then | |
| # if we were just going to cat the file, better auto with filename | |
| "$@" "$file" | |
| else | |
| $fmt "$file" | "$@" | |
| fi |
| minbase: | |
| build_only: true | |
| from: | |
| type: docker | |
| url: ${{DOCKER_BASE:docker://docker.io/library/}}ubuntu:jammy | |
| minboot: | |
| build_only: true | |
| from: | |
| type: built | |
| tag: minbase | |
| run: | | |
| apt-get update -q | |
| apt-get install --yes --no-install-recommends systemd-sysv | |
| talkrootfs: | |
| from: | |
| type: built | |
| tag: minboot | |
| run: | | |
| # password 'none' | |
| hashpass='$6$cpHPicKJ$O3lnesUxTQYSfuFtllAQZEf4xPjqSQMjV.hOCwATxUBm/oPVSA7zlTZSHffkXIDpIsW7/ec7chroIjo.7sV6i/' | |
| sed -i 's@^root:[^:]*:\(.*$\)@root:'"$hashpass"':\1@' /etc/shadow | |
| cat > /etc/rc.local <<"EOF" | |
| read up idle </proc/uptime | |
| echo "rc.local was run at $up seconds" > /run/rc.local.out | |
| EOF |
%title: Quick starting secure container storage using squashfs, overlay and dm-verity %author: Scott Moser %date: 2023-02-05
-> # Me
- Cisco
- Project-machine https://github.com/project-machine
- [email protected]
-> # Intro
- Goal is simple: Replace use of OCI images in tar+gz images with squasfs. ^
- Path there:
^
- Comparision of types stored in a registry ^
- Comparision of runtime use. ^
- Sales Pitch^H^H^H^H^H Demo
^
- Build --> Stacker ^
- Sign --> cosign ^
- Publish --> Zot ^
- Run --> lxc
-> # Registry Comparison ┌─────────────────────────────────────────────────────────────────────┐ │ │ │ Image Registry │ │ │ │ TAR.GZ │ SQUASHFS+VERITY │ │ ────────────────────────────┼─────────────────────────── │ │ - Registry stores OCI Images │ - Registry stores OCI Images │ │ │ │ │ - Images list layers │ - Images list layers │ │ │ │ │ - Layers are mediaType │ - Layers are mediaType │ │ oci.image. │ stacker.image. │ │ + layer.v1.tar+gzip │ + layer.squashfs+zstd+verity │ │ │ │ │ - signed checksum of tarball │ - signed checksum of image │ │ │ │ │ │ - signed dm-verity hash │ │ │ │ └─────────────────────────────────────────────────────────────────────┘
-> # Runtime Comparison ┌─────────────────────────────────────────────────────────────────────┐ │ │ │ Container Use / Runtime │ │ │ │ TAR.GZ │ SQUASHFS+VERITY │ │ ────────────────────────────┼─────────────────────────── │ │ - layers uncompress and untar │ - layer (can be) used from oci or │ │ │ copied per-container │ │ │ │ │ - Compare to original tar │ - Compare via sha256sum │ │ file-by-file │ │ │ │ │ │ │ - Privileged mounts get dm-verity │ │ │ │ │ │ - Unpriv mount via squashfuse │ │ │ │ │ │ - FS does not implement write │ │ │ │ │ │ - less easily readable │ └─────────────────────────────────────────────────────────────────────┘
-> # Write support
Overlayfs
- View a series of images overlayed into a single view ^
- Support for whiteouts and copy-ups just like oci format. ^
- Overlay bugs still present but largely handled ^
- Changes to the write layer can be easily seen
-> # dm-verity
^
- transparent integrity checking of block devices
^
- root hash is signed as part of image metadata
^
- block device read-time validation rather than up front checksum.
^
- Requires root - no unprivileged use of device-mapper.
-> # Demo
^
-
Warning: Users sensitive to invented-here-software might want to divert their eyes. (It is good software though.) ^
-
Build (stacker) - https://github.com/project-stacker/stacker ^
-
Sign (cosign) ^
-
Publish (zot) - https://github.com/project-zot/zot ^
-
Run (lxc)
-> # Stacker
- OCI Image build tool ^
- Unprivileged Builds ^
- Can be used to provide a container to build your software in. ^
- 5 years development. ^
- Going through CNCF inclusion process
-> # Sign
- Sign the image with cosign. The dm-verity information is present
-> # Zot ^
- Single Binary, easy to develop against ^
- Unprivileged ^
- Just like docker.io ^
- CNCF Sandbox project
-> # Thanks / Questions
- God, Team, Family, Cisco, You
- https://github.com/project-machine
- Questions
- Scott Moser [email protected]
| http: | |
| # set this to literal 'address: ""' to listen on all interfaces. | |
| # https://github.com/project-zot/zot/issues/1063 | |
| address: "127.0.0.1" | |
| port: 5000 | |
| realm: "localhost-zot" | |
| tls: | |
| cert: certs/server-cert.pem | |
| key: certs/server-key.pem | |
| auth: | |
| htpasswd: | |
| path: "htpasswd.txt" | |
| accessControl: | |
| # by default, anonymous read. the zot user can read/write | |
| "**": | |
| anonymousPolicy: [read] | |
| defaultPolicy: [read] | |
| policies: | |
| - users: [zot] | |
| actions: [read, create, update, delete, detectManifestCollision] | |
| log: | |
| level: "debug" | |
| audit: "logs/audit.log" | |
| output: "logs/zot.log" | |
| storage: | |
| dedupe: true | |
| rootdirectory: "storage" |
| #!/bin/sh | |
| Usage() { | |
| cat <<EOF | |
| ${0##*/} [dir] | |
| run ran server serving dir on port. | |
| EOF | |
| } | |
| fail() { echo "$@" 1>&2; exit 1; } | |
| dir=${2:-$HOME/local-zot} | |
| [ "$1" = "-h" -o "$1" = "--help" ] && { Usage; exit 0; } | |
| dir=$( cd "$dir" && pwd) || fail "failed cd $dir" | |
| cd "$dir" || fail "failed cd $dir" | |
| log_d="$HOME/logs" | |
| log="${log_d}/zot.log" | |
| pidf="$log.pid" | |
| mkdir -p "$log_d" || fail "failed mkdir $log_d" | |
| if [ -f "$pidf" ]; then | |
| if read p <"$pidf" && [ -n "$p" ] && [ -d "/proc/$p" ]; then | |
| echo "killing existing pid $p" | |
| kill $p | |
| sleep 1 | |
| fi | |
| fi | |
| config="config.yaml" | |
| zot="./bin/zot" | |
| if [ ! -x "$zot" ]; then | |
| zot=$(command -v "zot") 2>/dev/null || fail "no zot found" | |
| fi | |
| mkdir -p logs storage || fail "failed to make dirs in $PWD" | |
| [ -f "$config" ] || fail "no config '$config' in $PWD" | |
| [ -x "$zot" ] || fail "no zot bin '$zot' in $PWD" | |
| set -- $zot serve "$config" | |
| echo "$(date -R):" "writing to $log: $*" 1>&2 | |
| sh -ec ' | |
| pidf=$1; shift; echo $$ > "$pidf"; | |
| echo "# running in $PWD: $*" | |
| exec "$@"' -- \ | |
| "$pidf" "$@" </dev/null > "$log" 2>&1 & | |
| newpid=$! | |
| sleep 1 | |
| [ -d "/proc/$newpid" ] || { cat "$log" 1>&2; fail "failed to start $*"; } | |
| cat "$log" |
| #!/bin/bash | |
| # sync some docker images to zot | |
| # for these docker urls, then you can save yourself from | |
| # the docker bandwidth limit by just referencing | |
| # * docker://localhost:5000/docker-sync/ubuntu:latest | |
| # | |
| # Run this with some frequency, as up to date images are good to have. | |
| DOCKER_URLS=( | |
| "docker://ubuntu:jammy" | |
| "docker://busybox:latest" | |
| ) | |
| # Sync to a oci local directory to use less bandwidth from docker | |
| # and also to resume more safely. | |
| SYNC_OCI="oci:/tmp/sync-oci.d" | |
| ZOT_BASE="docker://localhost:5000/docker-sync" | |
| fail() { echo "$@" 1>&2; exit 1; } | |
| msg() { echo "$(date -R)" "$@"; } | |
| mkdir -p "${SYNC_OCI#oci:}" || | |
| fail "failed to create ${SYNC_OCI} dir for local sync" | |
| for dockurl in "${DOCKER_URLS[@]}"; do | |
| base=${dockurl##*/} | |
| ociurl="${SYNC_OCI}:$base" | |
| msg "sync $dockurl -> $ociurl" | |
| skopeo copy "$dockurl" "$ociurl" || | |
| fail "failed to sync $dockurl -> $ociurl" | |
| done | |
| for dockurl in "${DOCKER_URLS[@]}"; do | |
| base=${dockurl##*/} | |
| zoturl="${ZOT_BASE%/}/${base}" | |
| ociurl="${SYNC_OCI}:$base" | |
| msg "publish $ociurl -> $zoturl" | |
| skopeo copy --dest-tls-verify "$ociurl" "$zoturl" || | |
| fail "failed to publish $ociurl -> $zoturl" | |
| done | |
| msg "done" |