# Docker security ## 想解決的問題 主要還是提升 docker image 的安全性, 使用 alpine 的 image 會預設使用 root user, 如下: ``` sh docker run -it node:12-alpine sh / # whoami root ``` 即如果有人可以有任何方式 access 你的 image os, 他們都會是 root 權限等級. * [Command injection 範例](https://www.equalexperts.com/blog/tech-focus/docker-security-battle-of-the-base-image/) * 有開 child process 的 service 都蠻危險的 * Critical security issue: 如果 access 的權限是 root, 可以直接改掉你的 build file * Critical security issue: 如果你的 ENV 裡面有 MYSQL 的帳號密碼, 就可以直接 access ``` sh $ docker run -it node:12-alpine sh / # env NODE_VERSION=12.22.5 HOSTNAME=1effeac4240c YARN_VERSION=1.22.5 SHLVL=2 HOME=/root MYSQL_PASS=root1234 TERM=xterm PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin MYSQL_USER=root PWD=/ ``` * 有一些 container management solution 不允許使用 root 權限執行 container > Furthermore, your execution environment might block containers running as root by default (i.e., Openshift requires additional SecurityContextConstraints). ## 解決方法 ### chang alpine image user #### as-is 不指定 contianer user ``` sh FROM node:12-alpine WORKDIR /opt/app COPY .npmrc ./ COPY package*.json ./ COPY ecosystem.config.js ./ COPY config ./config COPY dist ./dist RUN npm install --only=production EXPOSE 3000 CMD [ "npm", "start" ] ``` #### to-be 建立一個 user, 並將並賦予該 user 特定的權限: * Assign user to owner ``` sh FROM node:12-alpine WORKDIR /opt/app RUN adduser -D jc && chown -R jc:jc /opt/app USER jc // 指定 owner 為 user COPY --chown=jc:jc .npmrc ./ COPY --chown=jc:jc package*.json ./ COPY --chown=jc:jc ecosystem.config.js ./ COPY --chown=jc:jc config ./config COPY --chown=jc:jc dist ./dist RUN npm install --only=production EXPOSE 3000 CMD [ "npm", "start" ] ``` ``` sh $ mv app.js app-1.js ``` * Default owner root ``` sh FROM node:12-alpine WORKDIR /opt/app RUN adduser -D jc && chown -R jc:jc /opt/app USER jc # 不指定 owner COPY .npmrc ./ COPY package*.json ./ COPY ecosystem.config.js ./ COPY config ./config COPY dist ./dist RUN npm install --only=production EXPOSE 3000 CMD [ "npm", "start" ] ``` ``` sh $ mv app.js app-1.js mv: can't rename 'app.js': Permission denied ``` 但是如果 EC2 被攻破了... 還是可以用 root 去 exec ``` $ docker exec -u 0 -it e1bf87409bc0 sh /opt/app # whoami root ``` ### distroless image > "Distroless" images contain only your application and its runtime dependencies. They do not contain package managers, shells or any other programs you would expect to find in a standard Linux distribution. #### 優點 * Base image size 比較小 * 只有留下必要的功能 #### 測試 如果使用 distroless image, 連 sh 的 module 都沒有, 如下: ``` sh $ docker run -it gcr.io/distroless/nodejs:12 sh internal/modules/cjs/loader.js:818 throw err; ^ Error: Cannot find module '/sh' at Function.Module._resolveFilename (internal/modules/cjs/loader.js:815:15) at Function.Module._load (internal/modules/cjs/loader.js:667:27) at Function.executeUserEntryPoint [as runMain] (internal/modules/run_main.js:60:12) at internal/main/run_main_module.js:17:47 { code: 'MODULE_NOT_FOUND', requireStack: [] } ``` 使用 multi-stage build image 階段執行 `RUN whoami`, 如下: ``` sh docker build . -t test_ad_distroless1 [+] Building 148.9s (17/17) FINISHED => [internal] load build definition from Dockerfile 0.0s => => transferring dockerfile: 44B 0.0s => [internal] load .dockerignore 0.0s => => transferring context: 35B 0.0s => [internal] load metadata for docker.io/library/node:12-alpine 0.0s => [internal] load metadata for gcr.io/distroless/nodejs:12 0.0s => [node-builder 1/8] FROM docker.io/library/node:12-alpine 0.0s => [internal] load build context 0.0s => => transferring context: 16.65kB 0.0s => [stage-1 1/4] FROM gcr.io/distroless/nodejs:12 0.0s => CACHED [node-builder 2/8] WORKDIR /opt/app 0.0s => CACHED [node-builder 3/8] COPY .npmrc ./ 0.0s => CACHED [node-builder 4/8] COPY package*.json ./ 0.0s => CACHED [node-builder 5/8] COPY ecosystem.config.js ./ 0.0s => CACHED [node-builder 6/8] COPY config ./config 0.0s => CACHED [node-builder 7/8] COPY dist ./dist 0.0s => [node-builder 8/8] RUN npm install --only=production 95.7s => CACHED [stage-1 2/4] COPY --from=node-builder /opt/app /opt/app 0.0s => CACHED [stage-1 3/4] WORKDIR /opt/app 0.0s => ERROR [stage-1 4/4] RUN whoami 1.2s ------ > [stage-1 4/4] RUN whoami: #17 1.125 container_linux.go:367: starting container process caused: exec: "/bin/sh": stat /bin/sh: no such file or directory ------ executor failed running [/bin/sh -c whoami]: exit code: 1 ``` ## 能直接改 base image 就好嗎 因為 distroless image 裡面執行 npm install 會噴以下錯誤 ``` sh => ERROR [8/8] RUN npm install --only=production 0.4s ------ > [8/8] RUN npm install --only=production: #12 0.354 container_linux.go:367: starting container process caused: exec: "/bin/sh": stat /bin/sh: no such file or directory ------ executor failed running [/bin/sh -c npm install --only=production]: exit code: 1 ``` image 裡面沒有任何 sh, 所以必須使用 multi stage, 在 node:12 做完所有的事情後, 丟進去 distroless image. ## Docker multi-stage ### 目的 * 讓 image size 變小 [資料來源](https://www.cnblogs.com/sparkdev/p/8594602.html) * 讓 build image 的流程變簡單, 不需要另外寫 shell script, 或是讓 RUN 的指令很複雜(容易出錯) > Notice that this example also artificially compresses two RUN commands together using the Bash && operator, to avoid creating an additional layer in the image. This is failure-prone and hard to maintain. It’s easy to insert another command and forget to continue the line using the \ character [資料來源](https://docs.docker.com/develop/develop-images/multistage-build/) ### 原理 讓你使用別的 image builder 產生的 artifacts 到自己的 image build stage 中使用. ## 使用 distroless 真的會讓 image 比較小嗎 ### 比較 base image (nodejs environment) alpine node 反而比較小 ``` sh $ docker images REPOSITORY TAG IMAGE ID CREATED SIZE node 12-alpine dc1848067319 4 days ago 88.9MB gcr.io/distroless/nodejs 12 c34c8c7e8e07 51 years ago 95.5MB ``` ### 比較 Dockerfile * Alpine + node12: ``` dockerfile FROM node:12-alpine WORKDIR /opt/app COPY .npmrc ./ COPY package*.json ./ COPY ecosystem.config.js ./ COPY config ./config COPY dist ./dist RUN npm install --only=production EXPOSE 3000 CMD [ "npm", "start" ] ``` * Multi-stage distroless + node12: ``` dockerfile FROM node:12-alpine AS node-builder WORKDIR /opt/app COPY .npmrc ./ COPY package*.json ./ COPY ecosystem.config.js ./ COPY config ./config COPY dist ./dist RUN npm install --only=production FROM gcr.io/distroless/nodejs:12 COPY --from=node-builder /opt/app /opt/app WORKDIR /opt/app CMD [ "dist/app" ] ``` ### 比較 dockerfile build 之後的 image 大小沒有差很多, 大約 30 mb. ``` sh $ docker images REPOSITORY TAG IMAGE ID CREATED SIZE test_ad_distroless latest 81e731307906 15 hours ago 506MB test_ad3 latest ef1cdbce0ede 18 hours ago 537MB ``` > note: Base on distroless multi-stage build 後的 image size 反而還比較小, 原因是因為 alpine build 的 COPY layer 比較多. ## CICD * 拉固定版本的 image * 先拉 base image, [驗證 image](https://github.com/GoogleContainerTools/distroless#how-do-i-verify-distroless-images), 再開始 build solution image ## Things IDK ### list user in Linux [參考資料](https://devconnected.com/how-to-list-users-and-groups-on-linux/) ### add user to Linux ### permission design in Linux [參考資料](https://blog.gtwang.org/linux/linux-chown-command-tutorial/) ### 什麼是 Openshift [參考資料](https://kknews.cc/zh-tw/tech/oepk52p.html) ### Layer 所有在 dockerfile 的指令都會是一個 read-only layer, 但是當我產生一個 container 的時候, 會自動加上去一個 writable 的 layer 在 底層 layer 上面(?). > When you run an image and generate a container, you add a new writable layer (the “container layer”) on top of the underlying layers. All changes made to the running container, such as writing new files, modifying existing files, and deleting files, are written to this writable container layer. ### RUN and CMD 指令差別 使用 RUN 是在 build docker image 的時候就已經下過的指令, CMD 則是在 run docker image 的時候會自動帶上去的指令 [參考](https://stackoverflow.com/questions/37461868/difference-between-run-and-cmd-in-a-dockerfile#:~:text=RUN%20is%20an%20image%20build,you%20launch%20the%20built%20image) ### Dockerfile EXPOSE 與 -p 指令差別 EXPOSE 僅僅打開 docker network 的 port, 沒有打開對外面的 port. [參考資料](https://blog.csdn.net/weixin_43944305/article/details/103116557#:~:text=Dockerfile%20%E9%87%8C%E9%9D%A2%E7%9A%84expose%EF%BC%8C%E6%98%AF,%2Dp%20%3A%20%E8%BF%99%E7%A7%8D%E5%BD%A2%E5%BC%8F%E3%80%82)