2021年10月12日火曜日

adnanh webhook を使ってコマンドを実行する方法

adnanh webhook を使ってコマンドを実行する方法

概要

過去にshell2http というツールを使って HTTP からコマンドを発行する方法を紹介しました

今回紹介する adnanh/webhook は更に柔軟にコマンドを発行することができます
インストールから簡単な使い方を紹介します

環境

  • Ubuntu 18.04
  • webhook 2.5.0

インストール

今回は Ubuntu なので apt を使います

  • apt-get -y install webhook

go で書かれているので go get したり自分でビルドしても OK です
apt でインストールすると自動で sysetmd 配下に登録してくれます

実行するシェルの作成

今回は簡単なシェルを作成します
現在の日付をファイルに出力するだけのスクリプトになります

#!/bin/bash
date > /tmp/result_test_shell.log

hooks.json の作成

hooks.json は webhook で使用する設定ファイルです
実行するスクリプト名や各種オプションを設定します
YAML 形式でも記載できます

  • cd /tmp
  • vim hooks.json
[
  {
    "id": "test-webhook",
    "execute-command": "/tmp/test.sh",
    "command-working-directory": "/tmp"
  }
]

複数の定義を記載することができます
id の部分は webhook でリクエストする際の URI のパス名になります

webhook の起動

では起動します
成功すると :9000 ポートで起動します

  • webhook -hooks /tmp/hooks.json -verbose

-verbose を付与するとログが表示されます
systemd がすでに起動している可能性もあるのでその場合は systemd 側を停止してから起動しましょう

  • systemctl stop webhook
  • systemctl disable webhook

動作確認

あとは curl で動作してみましょう
パラメータがない場合は GET でも POST でも OK です

  • curl localhost:9000/hooks/test-webhook
  • cat /tmp/result_test_shell.log

実行後にちゃんとファイルが生成されて日付が記載されていることを確認しましょう

パラメータを付与してスクリプトを実行する方法

webhook 時に application/json で渡したデータをスクリプト内で使用することもできます

#!/bin/bash
echo $MSG > result_test2_shell.log

MSG という変数に webhook で渡ってきたデータを格納してみます

パラメータのマッピングを定義する

webhook から送信された JSON 情報を変数に格納するマッピング情報を新たに定義します
使用するオプションは pass-environment-to-command になります

  • vim hooks2.json
[
  {
    "id": "test-webhook2",
    "execute-command": "/tmp/test2.sh",
    "command-working-directory": "/tmp",
    "include-command-output-in-response": true,
    "pass-environment-to-command":
    [
      {
        "envname": "MSG",
        "source": "payload",
        "name": "message"
      }
    ]
  }
]

source の payload は送られて来た JSON をパースすることを明示しています
JSON を使う場合は必須なのでそのまま記載します

そして JSON の message として送られてきたデータを変数 MSG に格納します
name がパース対象のフィールド名で envname が格納する変数名になります

動作確認

  • webhook -hooks /tmp/hooks2.json -verbose

ではデータを送信してみます
今度は POST で application/json のデータを送信するようにリクエストを変更します

  • curl -XPOST -H 'Content-type: application/json' -d '{"message":"HELLO"}' localhost:9000/hooks/test-webhook2

これでファイルを確認すると「HELLO」と記載されているのが確認できると思います

最後に

webhook を送信するサービスはいろいろとありますがそれを受け取る場合に自分で作成する必要がありました

adnanh/webhook を使えばどんなサービスの webhook でも受けられるように汎用的に作られているので便利です

他にも XML を受け取ったりファイル、マルチパートデータを受け取ったりできるので基本な HTTP リクエストには対応しているかなと思います

参考サイト

2021年10月11日月曜日

Prometheus で CPU 使用率が高くなったら Slack に通知するサンプル

Prometheus で CPU 使用率が高くなったら Slack に通知するサンプル

概要

Prometheus + Alertmanager + Node Export でノードの CPU 使用率を見て一定以上になったら Slack に通知するサンプルです

よく使う形式なので雛形として残しておきます

環境

  • Ubuntu 18.04
  • docker 20.10.7
  • Prometheus 2.30.3
  • Alertmanager 0.23.0
  • Node export 1.2.2

Node export 起動

  • docker run -d --net="host" --pid="host" -v "/:/host:ro,rslave" quay.io/prometheus/node-exporter:latest --path.rootfs=/host

:9100 ポートで起動します

Alertmanager 起動

  • mkdir data
  • vim alertmanager.yml
route:
  receiver: 'slack'
receivers:
- name: 'slack'
  slack_configs:
    - api_url: 'https://hooks.slack.com/services/xxx/xxx/xxx'
      channel: '#private'
      text: "{{ .CommonAnnotations.summary }}"
      send_resolved: true
  • docker run -d -p 9093:9093 --name alertmanager -v $(pwd):/alertmanager prom/alertmanager --config.file=/alertmanager/alertmanager.yml

:9093 ポートで起動します

Prometheus 起動

  • vim prometheus.yml
global:
  scrape_interval:     15s
  evaluation_interval: 15s

alerting:
  alertmanagers:
  - static_configs:
    - targets:
      - 192.168.100.10:9093

rule_files:
  - 'alerts.rule'

scrape_configs:
  - job_name: 'node1'
    static_configs:
    - targets: ['192.168.100.10:9100']
  • vim alerts.rule
groups:
  - name: 'Node1 cpu monitoring'
    rules:
    - alert: 'Node1 cpu usage alert'
      expr: avg without(cpu) (rate(node_cpu_seconds_total{ job="node1",mode!="idle"}[1m])) >= 0.4
      for: 5s
      annotations:
        summary: 'Node1 cpu usage over 40%'
  • docker run -d -p 9090:9090 --name prometheus -v $(pwd):/prometheus-data prom/prometheus --config.file=/prometheus-data/prometheus.yml

:9090 ポートで起動します

動作確認

CPU に負荷テストします

  • stress-ng -c 2 -l 100

-c オプションは CPU のコア数です
各自の環境に合わせて変更してください

これで Slack に通知が来れば OK です

最後に

必要最低限な設定としてセットで覚えちゃってもいいかもしれません

stress-ng で 100% 負荷なのに Prometheus 的には 40% ほぼの負荷にしかなっていなかったのは CPU のモードごとに Prometheus が管理しているだと思います

stress-ng 実行後に top コマンドでコアごとの使用率を見ると 100% だったので top と同じ値を監視したい場合は監視ルールを少し変更する必要がありそうです

2021年10月8日金曜日

uWSGI 超入門

uWSGI 超入門

概要

uWSGI は Webアプリケーションの前段に配置してデーモンやログの管理、リクエスト情報の加工などができます

今回は uWSGI のインストールから簡単な Webアプリケーションとの連携として flask と連携するのを試してみたいと思います

環境

  • macOS 11.6
  • Python 3.8.3
  • flask 2.0.2
  • uWSGI 2.0.19.1

インストール

  • pipenv install flask uWSGI

uWSGI だけで動かす

WSGI だけでも動作しますが基本的にはこの使い方はしません

def application(env, start_response):
    start_response('200 OK', [('Content-Type','text/html')])
    return [b"ok"]
  • pipenv run uwsgi --http 0.0.0.0:8080 --wsgi-file app.py
  • curl localhost:8080

flask と動かす

flask と組み合わせてみましょう
とは言っても普通に flask アプリケーションを作成して起動コマンドを wsgi に変更するだけです

from flask import Flask

app = Flask(__name__)
 
@app.route('/')
def api_sample():
    return 'ok'

if __name__ == '__main__':
    app.run()
  • pipenv run uwsgi --http=0.0.0.0:8080 --wsgi-file=run.py --callable=app
  • curl localhost:8080

--callable で flask をアプリを指定する感じになります

uwsgi.ini で動かす

WSGI にはさまざまなオプションがありますデーモンにしたりリクエストの情報をログファイルに書き出すこともできます

とりあえず今回は起動設定を書きましょう

[uwsgi]
wsgi-file=run.py
callable=app
http=0.0.0.0:8080

uwsgi.ini を作成することで先程指定した起動オプションを設定ファイルの指定に変更することができます

  • pipenv run uwsgi uwsgi.ini
  • curl localhost:8080

その他オプション

https://uwsgi-docs.readthedocs.io/en/latest/Options.html に指定可能な様々なオプショがあります

https://uwsgi-docs.readthedocs.io/en/latest/Configuration.html オプションファイル内では変数などを使うこともできます

最後に

WSGI 実装の Webアプリケーションであれば uWSGI はどんな Webアプリケーションでも使えます

flask の場合は flask run で内部的に WSGI を起動していますが実際は WSGI を切り離してちゃんと flask と WSGI で別々で起動したほうがメンテナンス性もいいですよという感じになります

要は flask run ではチューニングされて WSGI を使っているということになります

参考サイト

2021年10月7日木曜日

Sinatra で自作 websocket サーバを実装する

Sinatra で自作 websocket サーバを実装する

概要

Sinatra を使って websocket サーバを実装する方法を紹介します sinatra-websocket という gem があるのでこれを使うと簡単に実装できます

環境

  • macOS 11.6
  • Ruby 3.0.2p107
    • sinatra 2.1.0

準備

  • bundle init
  • vim Gemfile
gem "sinatra"
gem "sinatra-websocket"
gem "thin"
  • bundle install

WebSocket サーバの実装

  • vim app.rb
require 'sinatra'
require 'sinatra-websocket'

class WSApp < Sinatra::Base

  set :sockets, []

  get '/' do
    if !request.websocket?
      erb :index
    else
      request.websocket do |ws|
        ws.onopen do
          ws.send("Hello World!")
          settings.sockets << ws
        end
        ws.onmessage do |msg|
          EM.next_tick do
            settings.sockets.each do |s|
              s.send("Echo message: " + msg)
            end
          end
        end
        ws.onclose do
          warn("websocket closed")
          settings.sockets.delete(ws)
        end
      end
    end
  end

end

少し解説

!request.websocket? でリクエストが websocket がどうか判断できます

ws.onopen で接続時の処理を記載できます

ws.onmessage でメッセージ受信時の処理を記載できます

ws.onclose で切断時の処理を記載できます

接続しているクライアントごとにソケットを管理する必要があるので settings.sockets の配列でソケットを管理します

今回は受け取ったメッセージに「Echo message:」を付与してクライアント側に送信しています

サーバ起動スクリプト

require './app'
run WSApp

動作確認

でサーバを起動します
動作確認は適当な Websocket クライアントを使います
今回はこちらを使いました (ブラウザの Extension などもあるのでそれでも OK です)

接続先は ws://localhost:9292 を指定しましょう

あとは接続して適当にメッセージを送信してみるとちゃんとメッセージが返ってくるのが確認できると思います

最後に

最近だと無料で使える MQTT/Websocket サービスがあるのでそれを使ったほうが良いかなと思います

自分でメッセージのハンドリングや加工をしたい場合には自作の websocket が役に立つと思います

参考サイト

2021年10月6日水曜日

Python で bcrypt 入門

Python で bcrypt 入門

概要

bcrypt は文字列をハッシュ化し暗号化する技術です
ハッシュ化後は元の値を抽出できない不可逆暗号化になります
今回は Python で bcrypt を試してみました

環境

  • macOS 11.6
  • Python 3.8.3
    • bcrypt 3.2.0

インストール

  • vim Pipfile
[packages]
bcrypt = "*"
  • pipenv install

パスワードのハッシュ化

まずはパスワードをハッシュ化します

import bcrypt
import sys

password = sys.argv[1].encode('utf-8')
hashed = bcrypt.hashpw(password, bcrypt.gensalt())
print(hashed)

実行するには以下のようにします

=> b'$2b$12$sF3y7QCwqw3dBUNwc/i7nedZD0Nai72h5Cjs764YBJLp.uOwBh6UO'

ハッシュ化したパスワードの整合性チェック

実際にパスワードが正しいかどうかチェックします
先程生成したハッシュ化パスワードは hashed に設定します

import bcrypt
import sys

password = sys.argv[1].encode('utf-8')
hashed = b'$2b$12$sF3y7QCwqw3dBUNwc/i7nedZD0Nai72h5Cjs764YBJLp.uOwBh6UO'

if bcrypt.checkpw(password, hashed):
    print("It Matches!")
else:
    print("It Does not Match :(")

マッチは bcrypt.checkpw を使います
実行結果は以下のようになります

% pipenv run python test.py fuga
It Does not Match :(
% pipenv run python test.py hoge
It Matches!

最後に

ユーザのパスワード入力を評価する際にアプリケーション側でパスワードを保持する場合などに使いましょう

参考サイト

2021年10月5日火曜日

MySQL でデータを暗号復号化する方法

MySQL でデータを暗号復号化する方法

概要

今回は AES_ENCRYPT を使った方法を紹介します

環境

  • macOS 11.6
  • MySQL 8.0.26

テーブル作成

暗号化するカラムは VARBINARY 型で作成するのがポイントです

create table user (id int primary key auto_increment, name varchar(20), password varbinary(255));

暗号化データ登録

encrypt_secret_key は好きな文字列で OK です 暗号化および復号化する際に必要なシークレット情報になります
今回は直接指定していますが事前にハッシュ化してそれを指定するほうがよりセキュアです

insert into user values (null, 'hawksnowlog', AES_ENCRYPT('password12345', SHA2('encrypt_secret_key',512)));

or

select SHA2('encrypt_secret_key',512));
insert into user values (null, 'hawksnowlog', AES_ENCRYPT('password12345', '07f4fcde88f57126755e1f58c78ef8c97cd24b210cb198ae18c19c5010322b23b510198a996cdc25039f99ff9eaf7e34fbda81b99c83621bcddeec225f1ecf5d'));

データを確認すると以下のような暗号化された データが登録されているのが確認できます

mysql> select * from user;
+----+-------------+------------------------------------+
| id | name        | password                           |
+----+-------------+------------------------------------+
|  2 | hawksnowlog | 0x7119DC241B2B245C4CBA5CBE9FC5E90B |
+----+-------------+------------------------------------+
1 row in set (0.00 sec)

データを復号化する

AES_DECRYPT の1つ目の引数はカラム名です 2つ目の引数で先程暗号化にしようしたシークレット情報を使います

またカラムは varbinary なのでこれを char にコンバートすることで人間にもわかる文字列に変換できます

select convert(AES_DECRYPT(`password`, SHA2('encrypt_secret_key',512)), char(255)) from user;

or

select convert(AES_DECRYPT(`password`, '07f4fcde88f57126755e1f58c78ef8c97cd24b210cb198ae18c19c5010322b23b510198a996cdc25039f99ff9eaf7e34fbda81b99c83621bcddeec225f1ecf5d'), char(255)) from user;

最後に

暗号化するカラムは varbinary で定義するほうが良いみたいです

varchart + hex を使って保存するケースも見ますが公式では推奨ではないようです

参考サイト

2021年10月4日月曜日

Ruby で復号化可能なパスワードの管理方法を考える

Ruby で復号化可能なパスワードの管理方法を考える

概要

コード上でパスワードなどの認証情報を使って外部にアクセスしたい場合があります
もし認証情報が暗号化されている場合は何かしらの方法を使って復号化する処理をコード上に記載する必要があります

今回は ActiveSupport::MessageEncryptor を使って暗号化/復号化された認証情報を扱う方法を紹介します

また復号化の際に重要な復号化キーの管理方法も考えます

環境

  • macOS 11.6
  • Ruby 3.0.2p107
  • activesupport 6.1.4.1

準備

  • bundle init
  • vim Gemfile
gem "activesupport"
  • bundle install

暗号化されたパスワードを生成する

まずは暗号化されたパスワードを生成します

require 'active_support'

key = "sk123456789012345678901234567890" # key must be 32 bytes
crypt = ActiveSupport::MessageEncryptor.new(key)
encrypted_pwd = crypt.encrypt_and_sign("hoge")

これを実行すると TTFaaENWcmdJTTAwSUpuN2g5QWwrZz09LS1yQWw3Ymt3bkdXR3lmc2I3VWNhdWFBPT0=--194ef2156f114d9d1154cdd22dfedc31d5144017 という暗号化されたパスワード情報を取得できます

この暗号化されたパスワードをデータベースなりコンフィグファイルなりコード上で管理しているとします

復号化する処理の部分

では今度は復号化する処理を考えます 基本的には以下で OK です

require 'active_support'

key = "sk123456789012345678901234567890" # key must be 32 bytes
crypt = ActiveSupport::MessageEncryptor.new(key)
encrypted_pwd = "TTFaaENWcmdJTTAwSUpuN2g5QWwrZz09LS1yQWw3Ymt3bkdXR3lmc2I3VWNhdWFBPT0=--194ef2156f114d9d1154cdd22dfedc31d5144017"

raw_password = crypt.decrypt_and_verify(encrypted_pwd)

復号化する場合には decrypt_and_verify を使います
これで元のパスワードを取得できます

大事なのは 32バイトのキー情報

もうわかるかと思いますが暗号化するキーと復号化するキーはどちらも同じものを使っています

上記のサンプルでは直接コード上に key を記載していますがこれではダメです
データベースなどにある暗号化されたパスワードが漏洩しただけであれば key がわからない限りパスワードは元に戻せませんがもしコードが見れる場合には key がわかってしまうので簡単に復号化できてしまいます

なので key はまた別の場所で管理するようにしましょう
例えば暗号化されたパスワードがあるデータベースでもなくコードが管理されているリポジトリでもない第三のリポジトリを使ってそこで管理する方法です

この場合であればデータベースとコードが見れる状態であっても第三のリポジトリにアクセスできないのであればパスワードの復号化は防げます

ただコード自体は第三のリポジトリで管理されている key が必ず必要になるのでコードがデプロイされているサーバなどに入れてしまうのであれば復号化を防ぐ手段はありません

暗号化キーと復号化キーを更に分ける方法を考える

今回の ActiveSupport::MessageEncryptor ではできませんが暗号化するキーと復号化キーするキーを分けることでもう少しセキュリティを強化することができます

暗号化キーと復号化キーをそれぞれ別のリポジトリで管理することで堅牢にすることはできますがこの方法も結局コードがデプロイされているサーバにログインされてしまえばどちらも見ることができるはずなので微妙な感じにはなります

あとは復号化キーは別のデータベースで管理するという方法もあります

キーを外部から取得する

コード上では必要ですが処理が終了したら必ずキーを削除するようにします

そして再度複合化が必要な場合にはリポジトリや API を使って外部からキー情報を取得して復号化します

こうすればサーバ上に複合化キーの情報は残らないのでサーバにログインされても多少はキーの漏洩を防ぐことができます

bcrypt を使う

そもそも復号化の必要がないような仕組みにするのもありだと思います

ただ外部アクセスするようなケースだと bcrypt を使うのは難しいかもしれません

最後に

すべてのケースを想定した堅牢なパスワード管理を考えるのは不可能な気もします

しかしサーバにログインされたりデータベースにアクセスできたりと局所的になケースであればキーなどの情報を別の場所に保存するなりの対処で漏洩は防げる可能性は増えるかもしれません

最低でも復号化キー情報などはコードに直接書かずに外部で保存するようにしたほうがいいかなと思います

参考サイト