Package Blocklists Are Not Foolproof

Package Blocklists Are Not Foolproof

As organizations progress in their software container adoption journeys, they realize that they need image scanning beyond simple vulnerability checks.  As security teams develop more sophisticated image policies, many implement package blocklists to keep unnecessary code such as curl and sshd out of their images. Curl can be a handy tool in development and debugging, but attackers can also use it to download malicious code into an otherwise trusted container.  There are two primary scenarios security teams want to protect against:

  • An attacker compromises a container that has curl in it and then uses curl to bring compromised code into the environment 
  • A developer uses curl to download unapproved code, configurations, or binaries from unvetted sources (e.g. random GitHub repositories) during the build process (this could be malicious or inadvertent)

For the first scenario, a simple blocklisting of the curl package will cover most cases.  If we can produce an image that we know curl is not installed on, we’ve effectively mitigated an entire class of potential attacks.

Note: The policy rules, Dockerfiles, and other files used in this article are available from GitHub

Blocklisting curl

Blocklisting packages is a pretty straightforward process.  We just need a simple policy rule:

Anchore Enterprise policy rule: Gate: packages Trigger: blacklist Parameter: name=curl Action: stop

We’ll build an example image with this Dockerfile to test the rule:

# example dockerfile that will use curl to download source, anchore
# will stop this with a simple package blocklist on curl

FROM alpine:latest   WORKDIR /
RUN apk update && apk add --no-cache build-base curl

# download source and build
RUN curl -o - https://codeload.github.com/kevinboone/solunar_cmdline/zip/master | unzip -d / -
RUN cd /solunar_cmdline-master && make clean && make && cp solunar /bin/solunar

HEALTHCHECK --timeout=10s CMD /bin/date || exit 1
USER 65534:65534
CMD ["-c", "London"]
ENTRYPOINT ["/bin/solunar"]

OK, let’s build, push, and scan the image.

[email protected] ~/curl_example# export ANCHORE_CLI_USER=admin
[email protected] ~/curl_example# export ANCHORE_CLI_PASS=foobar
[email protected] ~/curl_example#
 export ANCHORE_CLI_URL=http://anchore.example.com:8228/v1

[email protected] ~/curl_example# docker build -t 
pvnovarese/curl_example:simple .
Sending build context to Docker daemon  112.1kB
Step 1/12 : FROM alpine:latest
 ---> a24bb4013296

[...]

Successfully built 799a36c3cb2d
Successfully tagged pvnovarese/curl_example:simple

[email protected] ~/curl_example# docker push pvnovarese/curl_example:simple
The push refers to repository [docker.io/pvnovarese/curl_example]

[...]

[email protected] ~/curl_example# anchore-cli image add --dockerfile ./Dockerfile pvnovarese/curl_example:simple

[...]

Simple curl Example

As expected, the package blocklist caught the installed curl package and the image fails the policy evaluation.

Multi-stage Builds Add Complexity

We’ve increased our protection against developers using curl to bring unknown code from random places on the internet into our environment.

But what if the developer uses a multi-stage build?  If you’re not familiar with multi-stage builds, they are frequently used to create more compact docker images.  The most common pattern is that a first stage is used to build the software, then the binaries and other artifacts produced are transferred to the final stage, leaving behind the source code, build tools, and other bits that are vital to building the code but aren’t needed to run the code.  The build-stage container then is discarded and only the final lean container with the bare necessities moves on.

Since those intermediate-stage containers are ephemeral, Anchore Enterprise doesn’t have access to them and can only scan the final image.  Because of this, many things that happen during the actual build process can avoid detection.  A developer can install curl in the intermediate build-stage container, pull down unvetted code, and then copy a compromised binary to a final stage image without installing curl in that final image.  

### example multistage build - in this case, a simple package blocklist
### will NOT stop this, since curl only is installed in the intermediate
### "builder" image and doesn't exist in the final image.  To stop this,
### we can look for curl in the RUN commands in the Dockerfile.

### Stage 1
FROM alpine:latest as builder
WORKDIR /solunar_cmdline-master
RUN apk update && apk add --no-cache build-base curl

### Clone private repository
RUN curl -o - https://codeload.github.com/kevinboone/solunar_cmdline/zip/master | unzip -d / -
RUN make clean && make

### Stage 2
FROM alpine:latest

HEALTHCHECK NONE
WORKDIR /usr/local/bin
COPY --from=builder /solunar_cmdline-master/solunar /usr/local/bin/solunar


# if you want to use a particular localtime,
# uncomment this and set zoneinfo appropriately
# RUN apk add --no-cache tzdata bash && cp /usr/share/zoneinfo/America/Chicago /etc/localtime


USER 65534:65534
CMD ["-c", "London"]
ENTRYPOINT ["/usr/local/bin/solunar"]

The final image output from this example is a completely standard alpine:latest image with a single binary copied in. Our simple package blocklist won’t catch this: the multistage image passes the policy evaluation even though curl was installed and used as part of the build process. Only the final image is checked against the package blocklist.

Our sample image passes the policy evaluation even though curl was used in the build process because the package is not installed in the final image.

To increase our protection, we should check for RUN instructions in the Dockerfile that call curl in addition to our package blocklist rule.

two new policy rules are added Gate: dockerfile Trigger: instruction Parameter: instruction=RUN check=like value=.curl. Action: stop Gate: dockerfile Trigger: no dockerfile provided Action: stop

We’ve added two rules.  The first will fail the image on any RUN instruction in the Dockerfile that includes “curl”, and the second will fail the image if no Dockerfile is submitted with the image.  We then re-evaluate with this new policy bundle (note that we don’t need to re-scan, we’re just applying the new policy to the same image) and get the desired failure:

Note: No changes were made to the Dockerfile from the previous run, and we did not rebuild the image – we only changed the policy rules.

This time, our policy evaluation caught both the installation of curl into the intermediate container and the actual execution of curl to download the unauthorized code.  Either of these alone is enough to cause the policy evaluation to fail as desired.

Also, in this case, our package blocklist was not triggered, since the final image still doesn’t contain the curl package.

Conclusion

Package blocklists can be quite useful. In most cases, whether or not particular packages are present in an image is much less of a concern than how those images are constructed and used, so looking just at the final image isn’t enough.  Anchore Enterprise’s deep image introspection includes analysis of the Dockerfile used to create the image, which allows the policy engine to enforce more best practices than simple image inspection alone.

Policies, Dockerfiles, and Jenkinsfiles used for this article can be found in my GitHub.