Podmanを動かして学ぶ！rootlessコンテナ入門

## 目次
* はじめに
  * 想定読者
  * 環境
* コンテナエンジン(Docker,Podman)の違いについて
    * Dockerとは何か
    * では、Podmanとは何か
* rootlessコンテナについて
    * rootコンテナのセキュリティリスク
    * rootlessコンテナについて
    * rootlessコンテナの設定方法
        * ハンズオン手順
        * ディレクトリ構造
    * 工夫した点
* まとめ
* 参考文献
--- 


## はじめに
* 仮想化技術として、コンテナが主流となり、これまでコンテナに触れてこなかったアプリケーション開発者がコンテナを開発する機会も増えてきているのではないでしょうか。
* 今回はセキュアなコンテナの設計として重要な、rootlessコンテナの考え方を以下に整理しました！
    * コンテナエンジンとしてのrootless
    * コンテナイメージの実行ユーザーとしてのrootless
* 前者については、Dockerコマンドに類似するコマンドを、『デーモンレス且つrootless』で実行可能なPodmanについて触れています。
* 後者については、rootlessコンテナの考え方を取り上げ、Dockerfileを含めたサンプル実装を行いました。
* ぜひ一読ください！

--- 
### 想定読者
本記事の想定読者は以下です。
* 主にJavaを使用してWebアプリ開発をしているが、最近コンテナの設計に携わり始めた方
* Dockerを用いて、コンテナの起動や停止など簡単なコマンドを使ったことはあるが、実際にDockerfileを作成したことはない方
* セキュリティを意識してコンテナの設計を行いたいが、どこから手をつければよいかイメージがつかない方

--- 
### 環境
* Linux
    * RHEL9系(AWS EC2のAMIイメージを使用)
* Podman
    * version 4.6.1
* Java
    * openjdk 17.0.9 2023-10-17 LTS
    * OpenJDK Runtime Environment (Red_Hat-17.0.9.0.9-1) (build 17.0.9+9-LTS)
    * OpenJDK 64-Bit Server VM (Red_Hat-17.0.9.0.9-1) (build 17.0.9+9-LTS, mixed mode, sharing)
* Tomcat
    *  apache-tomcat-9.0.84

--- 

## コンテナエンジン(Docker,Podman)の違いについて

Dockerと(今回ハンズオンで使用する)Podmanについて説明します。

### Dockerとは何か
RedHat社のドキュメントから引用させていただきます。[[1]](https://www.redhat.com/ja/topics/containers/what-is-docker)
>ITソフトウェアの「Docker」とは、Linux® コンテナの作成と使用を可能にするコンテナ化テクノロジーのことです。

そう、DockerはLinuxコンテナの作成と使用を行うためのツールです。


### Dockerのアーキテクチャ

- 以下の図が分かりやすかったので、引用させていただきました。[[2]](https://rheb.hatenablog.com/entry/2020/07/16/podman_buidah_for_docker_users)

<p align="center">
  <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/693114/f968b285-2658-d071-73ba-3e5e4c340d47.png" />
</p>

* Dockerデーモンがコンテナレジストリ、コンテナイメージ、コンテナ、Linuxカーネルを利用したすべての作業を実施します。
* 私たちはCLIからDockerコマンドを実行するだけで、Dockerデーモンが上記の作業を実施してくれます。
* デーモンは操作性の観点で、メリットが大きいですが、以下のデメリットも有します。[[2]](https://rheb.hatenablog.com/entry/2020/07/16/podman_buidah_for_docker_users)
> * 1つのプロセスが1つの障害点になる可能性がある
> * このプロセスはすべての子プロセス(実行中のコンテナ)を所有する
> * 障害が発生した場合、孤児(orphan)となるプロセスが存在する
> * すべてのDockerの操作は、同一の完全なroot権限を持つユーザーによって行われなければならなかった



### では、Podmanとは何か
- Podmanについても、以下の図が分かりやすかったので、引用させていただきました。[[2]](https://rheb.hatenablog.com/entry/2020/07/16/podman_buidah_for_docker_users)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/693114/1ef72d86-5591-9953-f485-c8dd506f6804.png)

* Podmanを一言で言うと『`rootless`且つ`デーモンレス`でDockerに類似のコマンドを使用可能なコンテナエンジン』です。
* Dockerを使用する際、Dockerデーモンがイメージのpull,push,buildなどを行ってきましたが、Podmanはデーモンが不要です。
* Podmanコマンドを使用するたびに、プロセスが立ち上がり、Podmanコマンドが終了すると、プロセスがkillされます。
* Dockerはデーモンを使用するため、従来root権限が必要でしたが、[v20.10以降正式にrootlessモードでサポートしています](https://docs.docker.jp/engine/security/rootless.html)。
* 一方、Podmanはデフォルトでrootless起動になっています。



ここまでDockerとPodmanについて説明してきました。
* Podmanはrootlessであり、便利な機能も多いのですが、実際にはPodmanのコミュニティよりもDockerのコミュニティの方が大きく、企業が導入するには多少ハードルが高いことも否めません。また、PodmanはDockerに比べ、リリースサイクルも早いです。
* 一方で、RHELは、[現在Dockerを非推奨としており、コンテナエンジンとして、デフォルトでPodmanを使用](https://access.redhat.com/ja/solutions/7028712)するようになっています。必要があれば、適宜キャッチアップしていきましょう！



## rootlessコンテナの実装について

これまで、コンテナエンジンについて説明してきましたが、いよいよ本題です！
まず、rootコンテナのリスクについて、簡単に説明した後、rootlessコンテナのメリットとサンプル実装を説明します！


--- 
### rootコンテナのセキュリティリスク


コンテナはデフォルトでrootで実行されます。rootで実行されるコンテナのリスクは以下です。

1. ホストへのエスケープのリスク[[3]](https://container-security.dev/security/breakout-to-host.html)
    * root権限で実行されるコンテナは、コンテナブレイクアウトのリスクを高めます。これは、攻撃者がコンテナから脱出してホストシステムにアクセスすることを意味します。
1. 権限の濫用
    * コンテナがrootとして動作する場合、攻撃者はコンテナ内でより高い権限を得ることができ、システム全体に影響を与える可能性があります。
1. セキュリティ脆弱性の影響の増大
    * rootとして実行されるコンテナは、新しいセキュリティ脆弱性に対してより脆弱になります。これは、攻撃者がシステム全体を危険にさらす機会を提供するためです。
--- 

### rootlessコンテナについて

Dockerfileの`USER`コマンドを使用することで、コンテナの実行ユーザーを一般ユーザーに変更することができます。
上記、rootコンテナのリスクに対して、適切に権限を割り振られた一般ユーザーを使用するrootlessコンテナのメリットは以下です。

1. セキュリティの向上
    * 非rootコンテナは、コンテナがシステムの他の部分に与えることができる損害を限定します。万が一コンテナ内でセキュリティ侵害が発生しても、攻撃者がシステム全体に影響を及ぼすことは困難になります。
1. 攻撃面の縮小
    * 非rootユーザーとしてコンテナを実行することで、潜在的なセキュリティ脆弱性の影響を軽減できます。これにより、システム全体のセキュリティが強化されます。
1. 共有マシン上での安全な利用
    * 共有されたマシン上で、管理者権限なしでコンテナを実行することが可能になります。これにより、複数のユーザーが同じマシン上で安全にコンテナを使用できるようになります。

rootlessコンテナの利点が分かってきたのではないでしょうか！
実際に、rootlessコンテナを実装してみましょう。

--- 

### rootlessコンテナの設定方法


今回は`war名/hello`とリクエストすると、`hello tomcat`を返却する`simpleservlet`をTomcatにデプロイします。
まずは、rootコンテナを作るためのDockerfileを作成し、rootlessコンテナに変更していきます。
以下、「ハンズオン手順」について「事前作業」→「rootコンテナのビルドと起動」→「rootコンテナのビルドと起動」の順に実行していきます。

:::note info

細かなコマンドを追っていただくのではなく、まずは大枠の理解とrootless対応後のDockerfileの内容の理解を優先していただければと思います！
:::


### ハンズオン手順



#### 事前作業
1. AWSのEC2サービスからRHELのAMIを選択し、インスタンスを起動後、当該インスタンスにSSH接続する。
1. `yum -y install podman`を実行する。
1. `podman -v`を実行し、podmanのversionが返却されることを確認する。
    * 返却例:`podman version 4.6.1`
1. 以下のディレクトリ構造に各種ファイルを配置する。

#### ディレクトリ構造 
```
simpleservlet(このディレクトリでコンテナをビルドする★)
├── Dockerfile
├── pom.xml
└── src
    └── main
        ├── java
        │   └── com
        │       └── example
        │           └── HelloServlet.java
        └── webapp
            ├── index.jsp(なくても動きます)
            └── WEB-INF
                └── web.xml
```


#### rootコンテナのビルドと起動

<details>
<summary>以下詳細</summary>
<div>


1. `podman build  -t simpleservlet:v.root .`を実行し、イメージを作成する。
1. `podman run -it simpleservlet:v.root`を実行し、コンテナが起動し、ターミナルにログが出ることを確認する。
1. `podman ps | grep "simpleservlet"` を実行する。実行結果のうち、2で起動したコンテナのコンテナIDをコピーする。
    * 以下の場合、「66a805f3c044」をコピーする。
    * `66a805f3c044  localhost/simpleservlet:v.root  catalina.sh run  37 seconds ago  Up 38 seconds              youthful_wilbur`
1. `podman exec -it {コンテナID} /bin/bash`を実行する`{コンテナID}`には、4で取得したコンテナIDを入れること。
1. rootユーザーでコンテナに入っていることを確認する。
1. `curl localhost:8080/HelloServlet/hello`を実行し、`hello tomcat`が返却されることを確認する。
1. `exit`でコンテナを抜ける。

</div>
</details>



#### rootlessコンテナのビルド起動

<details>
<summary>以下詳細</summary>
<div>

* 基本的には、「rootコンテナのビルドと起動」と同じ手順を試す。
* 以下のみ差分となるので注意。
* イメージbuild時のコマンドを以下のように変更する。
1. `podman build   --build-arg USER_NAME=$(whoami)   --build-arg GROUP_NAME=$(id -gn)   --build-arg USER_ID=$(id -u)   --build-arg GROUP_ID=$(id -g)   -t simpleservlet:v.rootless .`
* コンテナに入った際に作成した非rootユーザーで入っていることを確認する。

</div>
</details>


本題とは逸れるので詳しい説明は割愛しますが、実装したソースコードと、設定ファイルは以下です。
<details>
<summary>HelloServlet.java(web.xml記載のURLパターンで呼び出されるサーブレット)</summary>
<div>

```
package com.example;

import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class HelloServlet extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        response.setContentType("text/plain");
        response.getWriter().write("hello tomcat");
    }
}

```
</div>
</details>

<details>
<summary>web.xml(HelloServletが呼び出される際のURLパターンを記載)</summary>
<div>

```
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
                             http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
         version="4.0">

    <servlet>
        <servlet-name>HelloServlet</servlet-name>
        <servlet-class>com.example.HelloServlet</servlet-class>
    </servlet>

    <servlet-mapping>
        <servlet-name>HelloServlet</servlet-name>
        <url-pattern>/hello</url-pattern>
    </servlet-mapping>

</web-app>
```
</div>
</details>

<details>
<summary>pom.xml (依存関係を記載)</summary>
<div>

```
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.example</groupId>
    <artifactId>HelloServlet</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>war</packaging>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
    </properties>

    <dependencies>
        <!-- Servlet API -->
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>javax.servlet-api</artifactId>
            <version>3.1.0</version>
            <scope>provided</scope>
        </dependency>
    </dependencies>

    <build>
        <finalName>HelloServlet</finalName>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-war-plugin</artifactId>
                <version>3.2.3</version>
                <configuration>
                    <failOnMissingWebXml>true</failOnMissingWebXml>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>
```
</div>
</details>

<details>
<summary>rootless対応前のDockerfile</summary>
<div>

```
# 第1ステージ: Mavenを使用してWARファイルをビルド
# MavenのイメージはDocker Hub（docker.io/library）から取得
FROM docker.io/library/maven:3.8.4-openjdk-17-slim AS build

# Dockerイメージ内の作業ディレクトリを設定
WORKDIR /app

# MavenのPOMファイルをコピーし、依存関係をダウンロード（キャッシュのため）
COPY pom.xml .
RUN mvn dependency:go-offline

# ソースコードをコピーしてWARファイルをビルド
COPY src src
RUN mvn package -DskipTests

# 第2ステージ: RHELベースのイメージにTomcatをインストール
# RHELベースのJava 17イメージを使用
FROM registry.access.redhat.com/ubi8/ubi-minimal:latest

# システムを最新の状態に更新
RUN microdnf update && microdnf clean all

# Javaのインストール
ENV JAVA_VERSION 17.0.9.0.9-2.el8.x86_64
RUN microdnf install java-17-openjdk-${JAVA_VERSION} && \
    microdnf clean all
ENV JAVA_HOME /usr/lib/jvm/java-17-openjdk-${JAVA_VERSION}
ENV PATH $JAVA_HOME/bin:$PATH

# Tomcatのダウンロードとインストール
ENV TOMCAT_VERSION 9.0.84
RUN microdnf install wget tar vi && \
    wget https://downloads.apache.org/tomcat/tomcat-9/v${TOMCAT_VERSION}/bin/apache-tomcat-${TOMCAT_VERSION}.tar.gz && \
    tar xvf apache-tomcat-${TOMCAT_VERSION}.tar.gz && \
    mv apache-tomcat-${TOMCAT_VERSION} /usr/local/tomcat && \
    rm apache-tomcat-${TOMCAT_VERSION}.tar.gz && \
    microdnf clean all

# Tomcatの環境変数設定
ENV CATALINA_HOME /usr/local/tomcat
ENV PATH $CATALINA_HOME/bin:$PATH
RUN chmod -R 755 $CATALINA_HOME
WORKDIR $CATALINA_HOME

# curlのインストール
RUN microdnf install curl && \
    microdnf clean all

# ビルドステージからTomcatのwebappsディレクトリにWARファイルをコピー
# WARファイルの所有者を非rootユーザーに設定
COPY --chown=${USER_ID}:${GROUP_ID} --from=build /app/target/HelloServlet.war $CATALINA_HOME/webapps/

RUN chmod -R 0755 $CATALINA_HOME

# アプリケーションが実行されるポートを公開
EXPOSE 8080

# Tomcatサーバーを起動
CMD ["catalina.sh", "run"]
```
</div>
</details>

<details>
<summary>rootless対応後のDockerfile</summary>
<div>

```
# 第1ステージ: Mavenを使用してWARファイルをビルド
# MavenのイメージはDocker Hub（docker.io/library）から取得
FROM docker.io/library/maven:3.8.4-openjdk-17-slim AS build

# Dockerイメージ内の作業ディレクトリを設定
WORKDIR /app

# MavenのPOMファイルをコピーし、依存関係をダウンロード（キャッシュのため）
COPY pom.xml .
RUN mvn dependency:go-offline

# ソースコードをコピーしてWARファイルをビルド
COPY src src
RUN mvn package -DskipTests

# 第2ステージ: RHELベースのイメージにTomcatをインストール
# RHELベースのJava 17イメージを使用
FROM registry.access.redhat.com/ubi8/ubi-minimal:latest

# システムを最新の状態に更新
RUN microdnf update && microdnf clean all

# Javaのインストール
ENV JAVA_VERSION 17.0.9.0.9-2.el8.x86_64
RUN microdnf install java-17-openjdk-${JAVA_VERSION} && \
    microdnf clean all
ENV JAVA_HOME /usr/lib/jvm/java-17-openjdk-${JAVA_VERSION}
ENV PATH $JAVA_HOME/bin:$PATH
RUN chown -R ${USER_ID}:${GROUP_ID} $JAVA_HOME

# Tomcatのダウンロードとインストール
ENV TOMCAT_VERSION 9.0.84
RUN microdnf install wget tar vi && \
    wget https://downloads.apache.org/tomcat/tomcat-9/v${TOMCAT_VERSION}/bin/apache-tomcat-${TOMCAT_VERSION}.tar.gz && \
    tar xvf apache-tomcat-${TOMCAT_VERSION}.tar.gz && \
    mv apache-tomcat-${TOMCAT_VERSION} /usr/local/tomcat && \
    rm apache-tomcat-${TOMCAT_VERSION}.tar.gz && \
    microdnf clean all

# Tomcatの環境変数設定
ENV CATALINA_HOME /usr/local/tomcat
ENV PATH $CATALINA_HOME/bin:$PATH
RUN chmod -R 755 $CATALINA_HOME
WORKDIR $CATALINA_HOME

# curlのインストール
RUN microdnf install curl && \
    microdnf clean all

# ユーザーとグループの設定
ARG USER_NAME=tomcat
ARG GROUP_NAME=tomcat
ARG USER_ID=1000
ARG GROUP_ID=1000

# ユーザーとグループを作成し、ユーザーディレクトリを設定
RUN groupadd -g ${GROUP_ID} ${GROUP_NAME} && \
    useradd -m -u ${USER_ID} -g ${GROUP_NAME} -d /home/${USER_NAME} -s /bin/bash ${USER_NAME}

# Tomcatのディレクトリの所有権を設定
RUN chown -R ${USER_ID}:${GROUP_ID} $CATALINA_HOME

# ビルドステージからTomcatのwebappsディレクトリにWARファイルをコピー
# WARファイルの所有者を非rootユーザーに設定
COPY --chown=${USER_ID}:${GROUP_ID} --from=build /app/target/HelloServlet.war $CATALINA_HOME/webapps/

RUN chmod -R 0755 $CATALINA_HOME

# 非rootユーザーに切り替え
USER ${USER_NAME}

# アプリケーションが実行されるポートを公開
EXPOSE 8080

# Tomcatサーバーを起動
CMD ["catalina.sh", "run"]
```
</div>
</details>

* rootless対応後のコンテナに入って動作確認します。
* ホストOSのユーザーである`ec2-user`でログインできていることがわかりますね。
* web.xml通りにcurlすると、`hello tomcat`が正しく返却されています。


```
[ec2-user@8e73cd79d92d tomcat]$ curl localhost:8080/HelloServlet/hello
hello tomcat
```

### 工夫した点
* UIDとGIDをコマンドラインから入力できるようにした点
* 以下のようにbuildを実行することで、イメージビルド時にコンテナビルド時のユーザーと同じUID/GIDを持つユーザーを作成し、rootlessコンテナとすることができます。
* もちろん`$()`部分に直接文字列を埋め込むことも可能です。
```
podman build   --build-arg USER_NAME=$(whoami)   --build-arg GROUP_NAME=$(id -gn)   --build-arg USER_ID=$(id -u)   --build-arg GROUP_ID=$(id -g)   -t simpleservlet:v1.0 .
```


## まとめ

rootlessコンテナについて、コンテナエンジンとDockerfileの実装という観点で整理しました。本記事がrootlessコンテナを理解する上でお役に立てば幸いです。

## 参考文献
- [1] :https://www.redhat.com/ja/topics/containers/what-is-docker
- [2] :https://rheb.hatenablog.com/entry/2020/07/16/podman_buidah_for_docker_users
- [3] :https://container-security.dev/security/breakout-to-host.html