【Docker Desktopの代替】Docker Daemonを自分で立ててみた

みなさんご存知だとは思うのですが、Docker Desktopが有料化されるようですね。

www.itmedia.co.jp

そこで代替できるツールがないか調べてみたところ・・・

など、色々候補が上がりましたが、結局やりたいことってdockerデーモンの代替をどこに立てるか?ということだけなんだよな・・・と思い、今回はシンプルに自分で立ててみました。

ちなみに、Docker Machine は非推奨、hyperkit + minikube はマウントしたファイルの書き込み・読み込みが遅く、私が使っているアプリケーションでは実用に耐えられるレベルではありませんでした。

Podmanlima + docker はまだ検証してません。

今回実現したいこと

  • dockerコマンドでdockerの一通りの操作ができること
  • Docker Composeが使えること
  • 開発しているWebアプリケーション(Ruby, PHP)が設定変更なしにそのまま動作すること(権限周りなどは除く)

構成

ひとまず手元にあってシュッと使えそうなVagrantVirtualbox)を使いました。

設定

  • Vagrantfile

Dockerデーモンの乗ったサーバを作る

Vagrant.configure("2") do |config|
  config.vm.box = "bento/ubuntu-20.10"
  config.vm.box_version = "202103.19.0"

  config.vm.network "private_network", ip: "192.168.56.100"
  
  config.vm.provision "file", source: "./override.conf", destination: "/tmp/override.conf"

  repo_names.each do |repo_name|
    config.vm.synced_folder "#{ENV['HOME']}", "#{ENV['HOME']}",create:"true"
  end


  config.vm.provision "shell", inline: <<-SHELL
    apt update
    apt -y upgrade

    apt install -y docker.io

    mkdir -p /etc/systemd/system/docker.service.d/
    cp /tmp/override.conf /etc/systemd/system/docker.service.d/override.conf

    systemctl daemon-reload
    systemctl restart docker.service
  SHELL
end
  • override.conf
[Service]
ExecStart=
ExecStart=/usr/bin/dockerd -H tcp://192.168.56.100:2375 --tls=false
  • Dockerデーモンへのアクセス

環境変数 DOCKER_HOST にDockerデーモンの乗っているサーバのIPアドレスを指定すると使えるようになります。

export DOCKER_HOST="tcp://192.168.56.100:2375"

常時使う際は、これを .bash_profile などに設定しておくと良いかと思います。

設定のポイント

dockerクライアントとTCPで疎通させる

デフォルトではSocketを使った通信となりますが、起動時の設定を変えることでTCPでのやりとりができるようになります。

設定方法は次の項目で

Systemdの設定をオーバーライドした

/etc/systemd/system/docker.service.d/ 以下に設定でファイルを置くとsystemdの設定を上書きすることができます。 今回はTCPで起動したかったので、ExecStart=/usr/bin/dockerd -H tcp://192.168.56.100:2375 --tls=false で上書きしました。

TLSは今回はfalseにしていますが、必要に応じて設定すると良いかと思います。

ホスト側と同じディレクトリ構成でVMマウントした

Dockerマウントが必要なディレクトリについてはMacVMとで同じディレクトリ構成でマウントすることで、ホスト側のファイルが直接Dockerマウントされているように見せています。

使ってみた所感

Docker Desktopより速い

これまでDocker Desktopで立ち上げていたWebアプリケーションで試してみましたが、 今までより動きが速くなったことが体感レベルで感じられました。Docker Desktopはマウント周りが遅めなようなので、それが要因な気がします。

※アプリケーションのソースコードをマウントして使っていました。

パーミッションの設定を見直す必要がある

Docker Desktopはマウントしたファイルのパーミッションををいい感じにホスト側に合わせてくれます。 今回試したアプリケーションはDocker Desktop前提で作られており、アプリケーションから書き込みする権限がなかったので、適宜パーミッションの変更や空ボリュームを設定するなどの対応が必要でした。

ただ、NativeなDockerと同じ挙動となるので、個人的にはこれで良いかと。

まとめ

ファイルマウントやパーミッションについてはやや設定が面倒になりましたが、十分実用できそうで個人的には合格レベルでした。 もっと良い方法などあれば教えてください。

参考

MacでDocker Desktopを入れずにDocker CLI+Multipassで代替してみる

GitHub - 1021ky/self_dockermachine: 自前のdocker serverをVagrant+VirtualBoxで構築する

minioを使ってローカルにS3互換の環境を立ち上げてみた

ローカル環境でS3のテストを行うときに例えば以下の問題が考えられます。

  • 回線やサービスなどローカル環境以外の影響を受けることになるので障害時にテストすることができなくなる
  • 課金されてしまう
  • 公開したくないファイルをあげてしまう恐れがある

この問題を解決するために、ローカル環境でS3互換の環境を立ててくれるものはないか探してみたところ、以下のプロダクトが見つかりました。

min.io

これをDocker使って環境構築したところ、いくつか工夫すべきポイントがあったのでメモしておきます。

Dockerイメージ

minio公式のイメージを使いました。

hub.docker.com

Bitnami製もありこちらの方が環境変数で色々やってくれるので便利そうでしたが、 2021年9月時点での新しいバージョンのminioで使われている環境変数 MINIO_ROOT_USER MINIO_ROOT_PASSWORD を使用するとクラッシュしてしまうので公式イメージを使いました。

Entrypoint scripts do not support MINIO_ROOT_USER + MINIO_ROOT_PASSWORD · Issue #21 · bitnami/bitnami-docker-minio · GitHub

docker compose 設定

version: '3'

services:
  minio:
    image: minio/minio:latest
    container_name: "mybucket.minio"
    entrypoint: sh -c "
        mkdir -p /data/mybucket;
        mkdir -p /data/.minio.sys/buckets/mybucket;
        minio server /data --console-address ':37135'"
    ports:
      - 37135:37135 # 管理画面のポート
      - 9000:9000 # APIのポート
    environment:
      - MINIO_ROOT_USER=minio # 管理画面のユーザ、APIのアクセスキー
      - MINIO_ROOT_PASSWORD=password # 管理画面のパスワード、APIのシークレット
      - MINIO_DOMAIN=minio
    volumes:
      - minio:/data
volumes:
  minio:
  • container_name, MINIO_DOMAIN

こちらの設定は、2種類のアドレスモデル path-stylevirtual-hosted style 両方に対応できるようにしています。

container_nameで、mybucket.minio、サービス名に minio を設定することで、 dockerのネットワークから http://mybucket.minio:9000, http://minio:9000 両方でアクセスできるようになります。

  • entrypoint

ここではバケットの作成と管理画面のポートを指定しています。 mkdirでディレクトリを作成することで、バケットを作成することができます。 また、管理画面のポートはランダムできまるので、サービスを立ち上げる際に、--console-address ':37135' を指定することで 37135 で管理画面のポートを設定しています。

  • MINIO_ROOT_USER, MINIO_ROOT_PASSWORD

これは管理画面のユーザ、パスワードです。またAPIのアクセスキー、シークレットにも使えます。

  • volumes

データを永続化するため、ボリューム設定しています。

使ってみる

  • aws s3コマンド
# aws s3 --endpoint-url http://127.0.0.1:9000 ls --profile minio
2021-09-27 00:00:00 mybucket
  • s3cmd

こちらはXMLのパースエラーになってしまいました...

今回、コードは載せませんがファイルアップロードが成功したことを確認しました。


使ってみて何かあれば改めてエントリーを立てようと思います。今回はここまで。

ペネトレーションテストの教科書を読んだ

読んだ動機・目的

  • セキュリティテストの中でWebアプリケーションの脆弱性診断については、自分でも実施できるレベルになっているが、ペネトレーションテストについては概要レベルでしか理解できていなかったため
  • 実際の攻撃では具体的にどのようなツールを使っているのか興味があったため
    • セキュリティ対策をする上でも、実際にどのようなツールを使って攻撃されているのかがわかると一歩踏み込んで考えられると思ったから

書籍の目次

以下のURLをご参照ください https://scan.netsecurity.ne.jp/article/2021/08/04/46074.html

なるほどーと思ったツールや記事

evilginx2

フィッシングツール。リバースプロキシとして動作する。 二要素認証のフィッシングを行うことが可能

github.com

nuclei

テンプレートを用いたセキュリティスキャナー。既知の脆弱性や設定不備がないかをチェックすることができる。

github.com

git-all-secrets

指定した組織、組織の属するユーザのリポジトリ、gistから機密情報を探索するツール

github.com

metasploit

ペネトレーションテストツールのデファクトスタンダード

www.metasploit.com

サーバサイドテンプレートインジェクション テンプレートエンジンの特定方法

以下に記載されているフローで、どのテンプレートエンジンが使われているのかが特定できるとのこと。

portswigger.net

まとめ

ペネトレーションテストではどのような工程を踏んで行われているのか理解できたとともに、実際に使われているツール、またその使い方の概要を知ることができセキュリティ、特に攻撃に関する知見が広がって良かった。 今回は、一気に流し読みしてしまったので、次は各ツールを実際に使って試していこうと思う。

ARPスプーフィングしてNintendo Switchの通信を覗いてみた

Nintendo Switchある対戦ゲームで、ふと、どのような通信が行われているか気になったので、Wiresharkで通信を覗いてみました。

まだ、Wiresharkは使いこなせていないので、今回はパケットが来ていることを確認するところまでやります。

パケットキャプチャするための機材は持ち合わせていなかったので、ARPスプーフィングでノートPCから通信の内容を覗くことにしました。

事前準備

参考

こちらの記事を参考にさせていただきました https://www.kdryblog.com/arp-spoofing/#toc12

設定・インストール(PC)

ufwでポートを制限していたので、一時的に止めます。

sudo systemctl stop ufw

カーネルパラメータ net.ipv4.ip_forward を有効にします。

sudo sysctl -w net.ipv4.ip_forward=1

arpspoofコマンドをインストールします。(dsniffパッケージに含まれています)

sudo apt install dsniff

設定・インストール(Nintendo Switch)

IPアドレスが変わらないように固定にします。

ARPスプーフィングを行う

Nintendo SwitchIPアドレス192.168.100.200 とし、ルータのIPアドレス192.168.100.254 をします。

Nintendo Switchに向けて

sudo arpspoof -i eth0 -t 192.168.100.200 192.168.100.254

ルータに向けて

sudo arpspoof -i eth0 -t 192.168.100.254 192.168.100.200

実行すると、こんな感じで、継続的にターゲットへ偽の情報を送り続けます。

XX:XX:XX:XX:XX:XX XX:XX:XX:XX:XX:XX 0806 42: arp reply 192.168.100.254 is-at YY:YY:YY:YY:YY:YY
XX:XX:XX:XX:XX:XX XX:XX:XX:XX:XX:XX 0806 42: arp reply 192.168.100.254 is-at YY:YY:YY:YY:YY:YY
XX:XX:XX:XX:XX:XX XX:XX:XX:XX:XX:XX 0806 42: arp reply 192.168.100.254 is-at YY:YY:YY:YY:YY:YY
XX:XX:XX:XX:XX:XX XX:XX:XX:XX:XX:XX 0806 42: arp reply 192.168.100.254 is-at YY:YY:YY:YY:YY:YY
XX:XX:XX:XX:XX:XX XX:XX:XX:XX:XX:XX 0806 42: arp reply 192.168.100.254 is-at YY:YY:YY:YY:YY:YY

Wiresharkで確認する

Nintendo Switchで実際にゲームをしてみて、パケットが送信されていることを確認します。

f:id:takapi86:20210508184954p:plain

確認したゲームでは、対戦時P2Pでそれぞれ対戦相手のSwitchとUDPで直接やりとりしているようですね。(ルーターのNAT超えをしている模様)

まとめ

このような形で、簡単に通信Nintendo Switch通信を覗くことができました。 実際にやってみて、改めて暗号化された通信の重要性を感じました・・・ではでは。

iPhoneのHTTPリクエスト/レスポンスをキャプチャする

ふと、あるアプリがどのような形でサーバとやりとりしているのかが気になったので、iPhoneのHTTPリクエスト/レスポンスをキャプチャできるように設定してみました。

必要な機器・ソフト

  • iPhone
  • PC
  • Burp Suite
    • 今回はWindows10で行いましたが、MacOSLinuxでも問題ないと思います。

1. Burp Suiteのインストール(PCで操作)

Proxyサーバとして動かすため、以下からBurp Suiteをダウンロード・インストールします。 無償版であるCommunity EditionでOKです。

https://portswigger.net/burp/releases/professional-community-2021-4-2?requestededition=community

2. Burp Suiteの設定(PCで操作)

  • Proxy -> Interceptをoff
  • Proxy -> Proxy Listeners -> All interfacesを選択
    • Portは任意で設定してください。今回は 8082 にします。

3. PCに外部からアクセスを許可する(PCで操作)

  • パーソナルファイアウォール等を開け外部からアクセスできるようにします。
    • Windows10の場合は上記で、All interfacesを選択した際にファイアウォールの許可設定がダイアログで出てくるのでOKします。
    • Linuxの場合はufwiptablesなどを開けてあげてください
  • プライベートIPアドレスを調べる
    • ipconfigなどでプライベートIPアドレスを調べ控えておきます。(固定にしちゃっても良いかも)

4. Proxyを設定する(iPhoneで操作)

  • 設定 -> Wi-Fi -> 接続しているアクセスポイント -> プロキシを構成 -> PCのIPアドレスとポートを入力

5. 証明書を設定する(iPhoneで操作)

こちらの手順に沿って行います。

https://portswigger.net/support/installing-burp-suites-ca-certificate-in-an-ios-device

6. 動作確認をしてみる(PC、iPhoneで操作)

適当なサイトにアクセスして、ちゃんとProxyを経由しているか確認します。

Proxy -> HTTP historyにアクセスした履歴が残っていればOKです。

まとめ

こちらの手順でiPhoneのHTTPリクエスト/レスポンスが取得できるようになりました。 リクエストを改ざんして色々試したいときにも役に立つと思います。

今日はこの辺で

参考

[改訂版] iPhoneアプリのSSL接続をパケットキャプチャする方法

【メモ】CronJobについてざっくり概要を見ていった

業務で今VMのCronで動いているあるバッチをKubernetesのCronJobに移せないか検証するため、まずはCronJobについてざっくり概要を学んでいきたいと思います。

構築時に気をつけておきたい設定などをメモしていきます。

Kubernetesドキュメント

cronジョブは一度のスケジュール実行につき、 おおよそ 1つのジョブオブジェクトを作成します。ここで おおよそ と言っているのは、ある状況下では2つのジョブが作成される、もしくは1つも作成されない場合があるためです。通常、このようなことが起こらないようになっていますが、完全に防ぐことはできません。したがって、ジョブは 冪等 であるべきです。

まずは、移行対象のジョブが冪等であるか確認した方が良さそう。

Google Kubernetes Engineドキュメント

concurrencyPolicyがForbidに設定されている場合、前回のスケジュールがまだ実行中にCronJobをスケジュールしようとすると、CronJobは作成されません。

同時実行ポリシーは、Allow, Forbid, Replace何が適切なのかは構築前に検証した方がよいですね。

Kubernetes道場 14日目 - Job / CronJobについて

Jobが失敗して再起動処理を無限に続けないように、 backoffLimit というフィールドで制限回数を設けることが出来る。デフォルトは6回となっている。Podが失敗してbackoffする際に指数関数的に遅延を入れてから再度実行される。 Podが失敗してbackoffする際に指数関数的に遅延を入れてから再度実行される。

リトライどうするか設計もしなくては。

メルカリのエンジニアブログ Kubernetes CronJobと仲良くなりたい

failedJobsHistoryLimitは失敗終了したJobを指します。Jobが残るということはPodも残るので、特に失敗したJobからPodを特定しログを見ることで失敗原因を掴むことができます。

failedJobsHistoryLimitはデフォルト1のようですが、失敗原因を調べるのに情報が多い方が良いと思うので、増やしておいても良いかも。

100回スケジュールされる時間が経つと、CronJobはJobを生成できなくなるためCronJobの再作成が必要になります。

これは注意ですね。 動いていることを検知する仕組みがないと、動かないまま放置されてしまいそう。

ざっくり見て行ったので、次回は実際に手元で動かしてきます。

PowerDNSのAPIを使ってレコードを追加してみる

前回作成した、PowerDNS Authoritative Serverのdocker環境を使ってレコード情報を登録してみます。

レコードを追加する方法としては、以下の方法があります。

  • ビルトインのAPI経由で更新
  • nsupdateコマンド経由で更新
  • 直接SQLでDBに書き込んで更新

このうち、今回は ビルトインのAPI経由で更新 をやってみます。

参考情報

公式のドキュメントを参考に見ていきます。 https://doc.powerdns.com/authoritative/http-api/index.html

認証する

設定ファイル api-key に設定されている値を、リクエストヘッダに設定します。

curl -H 'X-API-Key: changeme' http://localhost:8081/api/v1/servers

zone情報を登録する

/servers/{server_id}/zones に向けてPOSTリクエストを送ります。 https://doc.powerdns.com/authoritative/http-api/zone.html#post--servers-server_id-zones

server_idは権威サーバーの場合は常にlocalhostを設定します。

簡単にRubyでクライアントを作成し実行してみます。

module Powerdns
  class Client
    def initialize(url, api_key)
      @connection = Faraday.new(
        url: url,
        headers: {
          'X-API-Key' => api_key,
        }
      )
    end

    def create_zone(zone_name)
      params = {
        "kind": "Master",
        "name": zone_name,
        "rrsets": [{
          "name": zone_name,
          "ttl": 3600,
          "type": "SOA",
          "records": [{
            "content": "dns1.example.jp. admin.example.com. 2020010101 900 600 6048000 300",
            "disabled": false
          }],
        }]
      }

      response = @connection.post do |request|
        request.url("/api/v1/servers/localhost/zones")
        request.headers['Content-Type'] = 'application/json'
        request.body = params.to_json
      end

      response
    end
  end
end

実行

client = Powerdns::Client.new(
      'http://localhost:8081',
      'api-key',
    )
client.create_zone("example.com.")

結果

201が返ってきた

{"account"=>"", "api_rectify"=>false, "dnssec"=>false, "edited_serial"=>2021010201, "id"=>"example.com.", "kind"=>"Master", "last_check"=>0, "master_tsig_key_ids"=>[], "masters"=>[], "name"=>"example.com.", "notified_serial"=>0, "nsec3narrow"=>false, "nsec3param"=>"", "rrsets"=>[{"comments"=>[], "name"=>"example.com.", "records"=>[{"content"=>"dns1.example.jp. admin.example.com. 2021010201 900 600 6048000 300", "disabled"=>false}], "ttl"=>3600, "type"=>"SOA"}], "serial"=>2021010201, "slave_tsig_key_ids"=>[], "soa_edit"=>"", "soa_edit_api"=>"DEFAULT", "url"=>"/api/v1/servers/localhost/zones/example.com."}

レコードを追加する

レコード追加用のエンドポイントはなく、zoneのエンドポイントに対しPATCHメソッドで更新する形のようです。

/servers/{server_id}/zones/{zone_id}

https://doc.powerdns.com/authoritative/http-api/zone.html#patch--servers-server_id-zones-zone_id

レコード追加用のメソッドを追加して実行してみます。

def add_records(name, type, ttl, content)
  params = {
    "rrsets": [
      {
        name: name,
        type: type,
        ttl: ttl,
        changetype: "REPLACE",
        comments: [],
        records: [
          {
            content: content,
            disabled: false
          }
        ],
      }
    ]
  }

  response = @connection.patch do |request|
    request.url("/api/v1/servers/localhost/zones/example.com.")
    request.headers['Content-Type'] = 'application/json'
    request.body = params.to_json
  end

  response
end

実行

client = Powerdns::Client.new(
      'http://localhost:8081',
      'api-key',
    )
client.add_records(
  "www.example.com.",
  "A",
  "300",
  "127.0.0.1",
)

結果

204が返ってきた

レスポンスボディはなし

問い合せをしてみる

Aレコードが登録されていることが確認できました。

dig +norec @localhost -p 1053 www.example.com ANY
;; Truncated, retrying in TCP mode.

; <<>> DiG 9.10.6 <<>> +norec @localhost -p 1053 www.example.com ANY
; (2 servers found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 28977
;; flags: qr aa; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 1232
;; QUESTION SECTION:
;www.example.com.               IN      ANY

;; ANSWER SECTION:
www.example.com.        300     IN      A       127.0.0.1

;; Query time: 5 msec
;; SERVER: ::1#1053(::1)
;; WHEN: Sat Jan 02 16:13:19 JST 2021
;; MSG SIZE  rcvd: 60

まとめ

PowerDNSのAPIを軽く触ってみましたが、シンプルでRESTfulなAPIなので使いやすかった印象でした。 細かい設定などは、別途引き続き見ていこうと思います。