Alkemist by RunSafe
SELF-SERVICE PORTAL

Summary

This document describes how to apply LFR to your own software. If you are interested in using software that has already been immunized by RunSafe, please refer to the ESP section.
If you are logged in, the examples on this page will automatically update to use your ALKEMIST_LICENSE_KEY.

General Steps

For most Docker build processes there are five steps necessary to integrate Alkemist®:
  1. Pull Alkemist LFR image from Docker Hub
  2. Include Alkemist LFR image as stage in existing build image
  3. Copy Alkemist LFR files out of LFR stage
  4. Build with Alkemist LFR
  5. Remove Alkemist LFR helper scripts from final image

Python Example

The clearest way to show how to integrate Alkemist LFR with code is to show an example. We will use Python 3.7.4 for this example. Below is the original Dockerfile for Python 3.7.4

  FROM alpine:3.10
  
  # ensure local python is preferred over distribution python
  ENV PATH /usr/local/bin:$PATH
  
  # http://bugs.python.org/issue19846
  # > At the moment, setting "LANG=C" on a Linux system *fundamentally breaks Python 3*, and that's not OK.
  ENV LANG C.UTF-8
  
  # install ca-certificates so that HTTPS works consistently
  # other runtime dependencies for Python are installed later
  RUN apk add --no-cache ca-certificates
  
  ENV GPG_KEY 0D96DF4D4110E5C43FBFB17F2D347EA6AA65421D
  ENV PYTHON_VERSION 3.7.4
  
  RUN set -ex     && apk add --no-cache --virtual .fetch-deps \
      gnupg \
      tar \
      xz \
    \
    && wget -O python.tar.xz "https://www.python.org/ftp/python/${PYTHON_VERSION%%[a-z]*}/Python-$PYTHON_VERSION.tar.xz" \
    && wget -O python.tar.xz.asc "https://www.python.org/ftp/python/${PYTHON_VERSION%%[a-z]*}/Python-$PYTHON_VERSION.tar.xz.asc" \
    && export GNUPGHOME="$(mktemp -d)" \
    && gpg --batch --keyserver ha.pool.sks-keyservers.net --recv-keys "$GPG_KEY" \
    && gpg --batch --verify python.tar.xz.asc python.tar.xz \
    && { command -v gpgconf > /dev/null && gpgconf --kill all || :; } \
    && rm -rf "$GNUPGHOME" python.tar.xz.asc \
    && mkdir -p /usr/src/python \
    && tar -xJC /usr/src/python --strip-components=1 -f python.tar.xz \
    && rm python.tar.xz \
    \
    && apk add --no-cache --virtual .build-deps  \
      bzip2-dev \
      coreutils \
      dpkg-dev dpkg \
      expat-dev \
      findutils \
      gcc \
      gdbm-dev \
      libc-dev \
      libffi-dev \
      libnsl-dev \
      libtirpc-dev \
      linux-headers \
      make \
      ncurses-dev \
      openssl-dev \
      pax-utils \
      readline-dev \
      sqlite-dev \
      tcl-dev \
      tk \
      tk-dev \
      util-linux-dev \
      xz-dev \
      zlib-dev \
  # add build deps before removing fetch deps in case there's overlap
    && apk del .fetch-deps \
    \
    && cd /usr/src/python \
    && gnuArch="$(dpkg-architecture --query DEB_BUILD_GNU_TYPE)" \
    && ./configure \
      --build="$gnuArch" \
      --enable-loadable-sqlite-extensions \
      --enable-optimizations \
      --enable-shared \
      --with-system-expat \
      --with-system-ffi \
      --without-ensurepip \
    && make -j "$(nproc)" \
  # set thread stack size to 1MB so we don't segfault before we hit sys.getrecursionlimit()
  # https://github.com/alpinelinux/aports/commit/2026e1259422d4e0cf92391ca2d3844356c649d0
      EXTRA_CFLAGS="-DTHREAD_STACK_SIZE=0x100000" \
  # https://github.com/docker-library/python/issues/160#issuecomment-509426916
      PROFILE_TASK='-m test.regrtest --pgo \
        test_array \
        test_base64 \
        test_binascii \
        test_binhex \
        test_binop \
        test_bytes \
        test_c_locale_coercion \
        test_class \
        test_cmath \
        test_codecs \
        test_compile \
        test_complex \
        test_csv \
        test_decimal \
        test_dict \
        test_float \
        test_fstring \
        test_hashlib \
        test_io \
        test_iter \
        test_json \
        test_long \
        test_math \
        test_memoryview \
        test_pickle \
        test_re \
        test_set \
        test_slice \
        test_struct \
        test_threading \
        test_time \
        test_traceback \
        test_unicode \
      ' \
    && make install \
    \
    && find /usr/local -type f -executable -not \( -name '*tkinter*' \) -exec scanelf --needed --nobanner --format '%n#p' '{}' ';' \
      | tr ',' '\n' \
      | sort -u \
      | awk 'system("[ -e /usr/local/lib/" $1 " ]") == 0 { next } { print "so:" $1 }' \
      | xargs -rt apk add --no-cache --virtual .python-rundeps \
    && apk del .build-deps \
    \
    && find /usr/local -depth \
      \( \
        \( -type d -a \( -name test -o -name tests \) \) \
        -o \
        \( -type f -a \( -name '*.pyc' -o -name '*.pyo' \) \) \
      \) -exec rm -rf '{}' + \
    && rm -rf /usr/src/python \
    \
    && python3 --version
  
  # make some useful symlinks that are expected to exist
  RUN cd /usr/local/bin \
    && ln -s idle3 idle \
    && ln -s pydoc3 pydoc \
    && ln -s python3 python \
    && ln -s python3-config python-config
  
  # if this is called "PIP_VERSION", pip explodes with "ValueError: invalid truth value '<VERSION>'"
  ENV PYTHON_PIP_VERSION 19.1.1
  
  RUN set -ex; \
    \
    wget -O get-pip.py 'https://bootstrap.pypa.io/get-pip.py'; \
    \
    python get-pip.py \
      --disable-pip-version-check \
      --no-cache-dir \
      "pip==$PYTHON_PIP_VERSION" \
    ; \
    pip --version; \
    \
    find /usr/local -depth \
      \( \
        \( -type d -a \( -name test -o -name tests \) \) \
        -o \
        \( -type f -a \( -name '*.pyc' -o -name '*.pyo' \) \) \
      \) -exec rm -rf '{}' +; \
    rm -f get-pip.py
  CMD ["python3"]
Adding LFR into this Dockerfile is as simple as three changes across four areas of the file. The diff for each of those changes is below, with an explanation following each.

index 90e7ba3..dc549a5 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,4 +1,6 @@
-FROM alpine:3.10
+FROM runsafesecurity/alkemist-lfr:alpine-3.10 AS lfr-files
+
+FROM alpine:3.10

# ensure local python is preferred over distribution python
ENV PATH /usr/local/bin:$PATH
@@ -14,6 +16,9 @@ RUN apk add --no-cache ca-certificates
ENV GPG_KEY 0D96DF4D4110E5C43FBFB17F2D347EA6AA65421D
ENV PYTHON_VERSION 3.7.4

+ENV LFR_ROOT_PATH=/lfr
+COPY --from=lfr-files /usr/src/lfr/ ${LFR_ROOT_PATH}
+
RUN set -ex \
        && apk add --no-cache --virtual .fetch-deps \
                gnupg \
@@ -69,7 +74,7 @@ RUN set -ex \
                --with-system-expat \
                --with-system-ffi \
                --without-ensurepip \
-       && make -j "$(nproc)" \
+       && ${LFR_ROOT_PATH}/scripts/lfr-helper.sh make -j "$(nproc)" \
 # set thread stack size to 1MB so we don't segfault before we hit sys.getrecursionlimit()
 # https://github.com/alpinelinux/aports/commit/2026e1259422d4e0cf92391ca2d3844356c649d0
                EXTRA_CFLAGS="-DTHREAD_STACK_SIZE=0x100000" \
@@ -109,7 +114,7 @@ RUN set -ex \
                        test_traceback \
                        test_unicode \
                ' \
-       && make install \
+       && ${LFR_ROOT_PATH}/scripts/lfr-helper.sh make install \
        \
        && find /usr/local -type f -executable -not \( -name '*tkinter*' \) -exec scanelf --needed --nobanner --format '%n#p' '{}' ';' \
                | tr ',' '\n' \
@@ -125,6 +130,7 @@ RUN set -ex \
                        \( -type f -a \( -name '*.pyc' -o -name '*.pyo' \) \) \
                \) -exec rm -rf '{}' + \
        && rm -rf /usr/src/python \
              
The file differences are only 4 new lines of code and 2 modified lines. The differences are:

Diff 1

  1. Import Alkemist LFR as a Docker build stage, using the tag which corresponds with your target environment
    • The image contains two docker ONBUILD commands which handle license verification and other pre-build steps
    • When building your image be sure to include a license key as detailed in the Specify License section below
@@ -1,4 +1,6 @@
-FROM alpine:3.10
+FROM runsafesecurity/alkemist-lfr:alpine-3.10 AS lfr-files
+
+FROM alpine:3.10

# ensure local python is preferred over distribution python

Diff 2

  1. Decide where to put the LFR files. This directory will be deleted at the end, so make sure it is not used by anything else
  2. Copy the LFR files from the LFR image into that directory
ENV GPG_KEY 0D96DF4D4110E5C43FBFB17F2D347EA6AA65421D
ENV PYTHON_VERSION 3.7.4
 
+ENV LFR_ROOT_PATH=/lfr
+COPY --from=lfr-files /usr/src/lfr/ ${LFR_ROOT_PATH}
+
RUN set -ex \

Diff 3

  1. Utilize the LFR helper script to detect the compiler. It will:
    • add arguments as necessary for LFR to function
    • ensure that the compiler will call LFR’s linker wrapper
    • otherwise maintain the original linker
Note: The helper script only needs to be added to steps that include compilation.

                --with-system-ffi \
                --without-ensurepip \
-       && make -j "$(nproc)" \
+       && ${LFR_ROOT_PATH}/scripts/lfr-helper.sh make -j "$(nproc)" \
 # set thread stack size to 1MB so we don't segfault before we hit sys.getrecursionlimit()
 # https://github.com/alpinelinux/aports/commit/2026e1259422d4e0cf92391ca2d3844356c649d0
 

        test_traceback \
        test_unicode \
        ' \
-       && make install \
+       && ${LFR_ROOT_PATH}/scripts/lfr-helper.sh make install \
        \
 

Diff 4

  1. Utilize the LFR cleanup script to remove files that were needed by LFR during the build process, but will not be needed at run-time.

        | xargs -r apt-mark manual \
    && apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false \
    && rm -rf /var/lib/apt/lists/* \
+   && ${LFR_ROOT_PATH}/scripts/lfr-cleanup.sh \
    \
    && find /usr/local -depth \
        \( \
 


Specifying License Key

Alkemist LFR expects your license key to be provided as an ALKEMIST_LICENSE_KEY build argument. The preferred way to do this is to specify your key as a build argument from the command line along with the current date. Dockerfile-lfr in the command below is assumed to point to a Dockerfile infused with the differences listed above.

$ docker build -t python-3.7.4-lfr -f Dockerfile-lfr --build-arg ALKEMIST_LICENSE_KEY="<insert Alkemist license here> $(date)"

Verify LFR Functionality

Verification that Alkemist LFR has been added to the finished binary can be done in a variety of ways. Here are two common approaches:
  • Spin up a container to run ldd against the protected binary and verify its output contains liblfr.so
    $ docker run python-lfr /bin/sh -c 'ldd $(which python)'
    /lib/ld-musl-x86_64.so.1 (0x7f0c4aad3000)
    libpython3.7m.so.1.0 => /usr/local/lib/libpython3.7m.so.1.0 (0x7f0c4a6e8000)
    libc.musl-x86_64.so.1 => /lib/ld-musl-x86_64.so.1 (0x7f0c4aad3000)
    liblfr.so => /usr/local/lib/liblfr.so (0x7f0c4a6d5000)
  • If ldd is not available: shell into the container, start Python, and verify that liblfr.so is mapped into the running process.
    $ docker exec -it python-3.7.4-lfr bash
    [email protected]:/# python &> /dev/null &
    [5] 19
    [email protected]:/# grep lfr /proc/19/maps
    7f6d679ba000-7f6d679cb000 r-xp 00000000 08:06 26216527                  /lib/lfr/x86_64/liblfr.so
    7f6d679cb000-7f6d67bca000 ---p 00011000 08:06 26216527                  /lib/lfr/x86_64/liblfr.so
    7f6d67bca000-7f6d67bcb000 r--p 00010000 08:06 26216527                  /lib/lfr/x86_64/liblfr.so
    7f6d67bcb000-7f6d67bcc000 rw-p 00011000 08:06 26216527                  /lib/lfr/x86_64/liblfr.so

Enterprise Software Protection

RunSafe Security follows the process above to build a collection of immunized software that can be easily deployed into production environments. The complete collection can be found at our alkemist-esp repository on Docker Hub.
Using the ESP images is as simple as substituting the FROM line in your Dockerfiles to use the runsafesecurity/alkemist-esp images that correspond with your current base images and providing your ALKEMIST_LICENSE_KEY as an environment variable to your containers based on the modified image.
  • Modify Dockerfile
    
    -FROM httpd:2.4.39-alpine
    +FROM runsafesecurity/alkemist-esp:httpd-2.4.39-alpine-3.10-lfr
                  
  • Provide ALKEMIST_LICENSE_KEY as environment variable
    
    $ docker run -e ALKEMIST_LICENSE_KEY=<insert Alkemist license here> <your image>