diff --git a/Rakefile b/Rakefile index c2fc748..4e70f9f 100644 --- a/Rakefile +++ b/Rakefile @@ -6,6 +6,9 @@ require 'net/http' require 'uri' require 'csv' +# 表示用の定数 +SEPARATOR_WIDTH = 60 # セパレーター行の幅 + # Minitestタスクの定義 Rake::TestTask.new(:test) do |t| t.libs << "test" @@ -49,10 +52,10 @@ task :default => :test desc "利用可能なDojoPaaS管理タスクをすべて表示" task :default do puts "\n🔧 DojoPaaS 管理タスク" - puts "=" * 50 + puts "=" * SEPARATOR_WIDTH puts "'rake -T' ですべての利用可能なタスクを確認" puts "'rake -D [タスク名]' で詳細な説明を表示" - puts "=" * 50 + puts "=" * SEPARATOR_WIDTH sh "rake -T" end @@ -127,7 +130,7 @@ namespace :server do puts "✅ 有効なIPアドレス: #{validated_ip_str}" puts "🔍 サーバー情報を検索中..." - puts "-" * 50 + puts "-" * SEPARATOR_WIDTH # 検証済みIPでinitialize_server.rbスクリプトを実行(コマンドエコーを抑制) sh "ruby scripts/initialize_server.rb --find #{validated_ip_str}", verbose: false @@ -153,7 +156,7 @@ namespace :server do puts "📋 Issue処理中: #{issue_url}" puts "🔍 サーバー情報を抽出中..." - puts "-" * 50 + puts "-" * SEPARATOR_WIDTH sh "ruby scripts/initialize_server.rb --find #{issue_url}", verbose: false end @@ -171,7 +174,7 @@ namespace :server do end puts "🔍 サーバー名で検索: #{name}" - puts "-" * 50 + puts "-" * SEPARATOR_WIDTH sh "ruby scripts/initialize_server.rb --find #{name}", verbose: false end @@ -319,25 +322,62 @@ namespace :server do desc "現在稼働中のサーバー一覧を表示" task :list do require_relative 'scripts/sakura_server_user_agent' - + require 'dotenv/load' + puts "📋 サーバー一覧を取得中..." puts "データソース: #{SakuraServerUserAgent::INSTANCES_CSV_URL}" - puts "-" * 50 - + puts "-" * SEPARATOR_WIDTH + begin uri = URI(SakuraServerUserAgent::INSTANCES_CSV_URL) response = Net::HTTP.get_response(uri) - + if response.code == '200' # エンコーディングを明示的に設定してCSVを解析(無効な文字を安全に処理) response.body.force_encoding('UTF-8').scrub('?') csv_data = CSV.parse(response.body, headers: true) + + # APIクライアントを初期化(ステータス確認用) + server_statuses = {} + if ENV['SACLOUD_ACCESS_TOKEN'] && ENV['SACLOUD_ACCESS_TOKEN_SECRET'] + begin + # デフォルトパラメータ(石狩第二)が自動的に使用される + client = SakuraServerUserAgent.new + servers_data = client.get_servers() + if servers_data && servers_data['Servers'] + servers_data['Servers'].each do |server| + server_statuses[server['Name']] = server['Instance']['Status'] + end + end + rescue + # エラーは無視してステータスなしで続行 + end + end puts "📊 サーバー一覧(#{csv_data.length}台):" puts "" - + csv_data.each do |row| - puts " 🖥️ #{row['Name']}" + server_name = row['Name'] + status = server_statuses[server_name] + + # ステータスに応じた絵文字と表示を設定 + status_display = if status + case status + when 'up' + " (✅ up)" + when 'down' + " (⏸️ down)" + when 'cleaning' + " (🧹 cleaning)" + else + " (❓ #{status})" # 予期しないステータスの場合 + end + else + "" # APIが利用できない場合は何も表示しない + end + + puts " 🖥️ #{server_name}#{status_display}" puts " IPアドレス: #{row['IP Address']}" # スペースを追加 puts " 説明: #{row['Description']}" if row['Description'] puts "" @@ -359,6 +399,15 @@ namespace :server do end puts "" + # ステータス表示についての注記 + if !ENV['SACLOUD_ACCESS_TOKEN'] || !ENV['SACLOUD_ACCESS_TOKEN_SECRET'] + puts "ℹ️ 注: API認証情報が設定されていないため、サーバーステータス(up/down)は表示されていません" + puts " ステータスを表示するには、SACLOUD_ACCESS_TOKEN と SACLOUD_ACCESS_TOKEN_SECRET を設定してください" + elsif server_statuses.empty? + puts "ℹ️ 注: API接続エラーのため、サーバーステータス(up/down)を取得できませんでした" + end + puts "" + else abort "❌ エラー: サーバー一覧の取得に失敗しました (HTTP #{response.code})" end @@ -367,6 +416,55 @@ namespace :server do abort "❌ エラー: #{e.message}" end end + + # ======================================== + # 個別サーバー作成タスク(テスト用) + # ======================================== + desc "指定したサーバーを個別に作成(テスト用)" + task :create, [:server_name] => [:check_api_credentials] do |t, args| + server_name = args[:server_name] + + unless server_name + abort "❌ エラー: サーバー名が必要です\n" \ + "使い方: rake server:create[coderdojo-japan]\n" \ + "注意: servers.csvに登録されているサーバー名を指定してください" + end + + puts "="*SEPARATOR_WIDTH + puts "🚀 DojoPaaS 個別サーバー作成" + puts "="*SEPARATOR_WIDTH + puts "" + puts "サーバー名: #{server_name}" + puts "" + + # deploy.rbのCoderDojoSakuraCLIクラスを使用(DRY原則) + require_relative 'scripts/deploy' + + cli = CoderDojoSakuraCLI.new([]) + success = cli.create_single_server(server_name) + + if success + puts "" + puts "="*SEPARATOR_WIDTH + puts "✅ サーバー作成プロセス完了" + puts "="*SEPARATOR_WIDTH + puts "" + puts "【次のステップ】" + puts "1. SSHで接続確認:" + puts " ssh ubuntu@" + puts "" + puts "2. スタートアップスクリプトの実行状況確認:" + puts " ssh ubuntu@ 'sudo tail -f /var/log/cloud-init-output.log'" + puts "" + else + puts "" + puts "="*SEPARATOR_WIDTH + puts "❌ サーバー作成に失敗しました" + puts "="*SEPARATOR_WIDTH + exit 1 + end + end + end # ヘルパーメソッド(将来の拡張用に保持) diff --git a/bin/deploy.sh b/bin/deploy.sh index 4c88caf..d84cc99 100644 --- a/bin/deploy.sh +++ b/bin/deploy.sh @@ -20,12 +20,5 @@ else COMMIT_MSG="Deploy from actions" fi -# 追加されたサーバー情報を取得(オプション) -NEW_SERVERS=$(git diff --cached instances.csv | grep '^+' | grep -v '^+++' | cut -d',' -f1 | sed 's/^+//' | head -3 | paste -sd ', ' || echo "") - -if [ -n "$NEW_SERVERS" ]; then - COMMIT_MSG="$COMMIT_MSG - Added: $NEW_SERVERS" -fi - git commit --quiet -m "$COMMIT_MSG" git push --force --quiet "${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}" main:gh-pages diff --git a/docs/security-defense-in-depth.md b/docs/security-defense-in-depth.md new file mode 100644 index 0000000..9ac0b17 --- /dev/null +++ b/docs/security-defense-in-depth.md @@ -0,0 +1,208 @@ +# DojoPaaS セキュリティ設計:多層防御(Defense in Depth) + +## 概要 + +DojoPaaSでは、CoderDojoのサーバーを保護するために**多層防御(Defense in Depth)**戦略を採用しています。これは、単一の防御機構に依存せず、複数の独立したセキュリティ層を重ねることで、より強固な防御を実現する設計思想です。 + +## 二層のファイアウォール構成 + +``` +インターネット + ↓ +[Layer 1: さくらのクラウド パケットフィルター] + ↓ +サーバー + ↓ +[Layer 2: iptables (サーバー内部)] + ↓ +アプリケーション +``` + +### Layer 1: パケットフィルター(クラウド側) + +**設定ID**: `112900922505` +**管理場所**: さくらのクラウドコントロールパネル +**特徴**: +- サーバーの外側でトラフィックを制御 +- サーバー起動前から有効 +- 複数サーバーで設定を共有可能 + +**開放ポート**: +- 22/tcp (SSH) +- 80/tcp (HTTP) +- 443/tcp (HTTPS) +- 1024-65535/tcp (高位ポート) +- 1024-65535/udp (高位ポート) + +### Layer 2: iptables(サーバー内部) + +**設定ファイル**: `startup-scripts/112900928939` +**管理場所**: サーバー内部(`/etc/iptables/rules.v4`) +**特徴**: +- サーバー内部でトラフィックを制御 +- より細かい制御が可能(rate limiting等) +- スタートアップスクリプトで自動設定 + +**セキュリティ機能**: +- SSH ブルートフォース攻撃対策(hashlimit) +- ICMP フラッド攻撃対策 +- TCP フラグ異常パケットのドロップ +- IPv6 完全ブロック + +## なぜ両方必要なのか? + +### 1. 時系列での保護 + +``` +サーバー起動 + ↓ [この間も保護が必要] +スタートアップスクリプト実行(数秒〜数分) + ↓ +iptables 有効化 +``` + +パケットフィルターは**サーバー起動直後から有効**なため、スタートアップスクリプトが実行されるまでの間もサーバーを保護します。 + +### 2. 障害時の冗長性 + +- パケットフィルターが無効になっても → iptablesが保護 +- iptables設定が失敗しても → パケットフィルターが保護 +- どちらか片方が機能していれば、最低限のセキュリティを維持 + +### 3. 管理の観点 + +| 項目 | パケットフィルター | iptables | +|------|------------------|----------| +| 設定変更 | Webコンソール/API | SSH接続が必要 | +| 複数サーバー管理 | 一括設定可能 | 個別設定必要 | +| 設定の複雑さ | シンプル | 複雑な制御可能 | +| 適用タイミング | 即座 | 再起動/reload必要 | + +### 4. 攻撃シナリオへの対策 + +**例:DDoS攻撃** +- Layer 1(パケットフィルター): 基本的なフィルタリング +- Layer 2(iptables): rate limitingで詳細な制御 + +**例:設定ミス** +- 管理者がiptablesを誤って無効化 → パケットフィルターが保護継続 +- パケットフィルターの設定ミス → iptablesが最後の砦 + +## SSH セキュリティの追加強化 + +スタートアップスクリプトでは、ファイアウォールに加えてSSH自体のセキュリティも強化: + +```bash +# rootログインを無効化 +PermitRootLogin no + +# パスワード認証を無効化(鍵認証のみ) +PasswordAuthentication no +``` + +## 実装コード + +### パケットフィルターの適用(Ruby) + +```ruby +# scripts/sakura_server_user_agent.rb +def apply_packet_filter(params = nil) + @interface_id ||= params[:interface_id] + @packet_filter_id ||= params[:packet_filter_id] + + # パケットフィルターIDが指定されていない場合はスキップ + if @packet_filter_id.nil? + puts "パケットフィルターは適用されません(packet_filter_id is nil)" + return + end + + response = send_request('put', + "interface/#{@interface_id}/to/packetfilter/#{@packet_filter_id}", nil) +end +``` + +### デフォルト設定 + +```ruby +# デフォルトで本番環境のパケットフィルターを適用 +def initialize(zone:"31002", packet_filter_id:'112900922505', ...) + @packet_filter_id = packet_filter_id + # ... +end +``` + +## テスト環境での無効化 + +開発・テスト環境では、必要に応じてパケットフィルターを無効化可能: + +```ruby +# テスト用スクリプト +ssua = SakuraServerUserAgent.new(packet_filter_id: nil) +``` + +⚠️ **警告**: 本番環境では絶対にパケットフィルターを無効化しないでください。 + +## トラブルシューティング + +### SSH接続できない場合 + +1. **パケットフィルター確認** + ```bash + ruby scripts/utils/check_packet_filter.rb + ``` + +2. **サーバーステータス確認** + ```bash + ruby scripts/utils/check_server_status.rb <サーバー名> + ``` + +3. **ポート疎通確認** + ```bash + curl -v telnet://:22 --max-time 3 + ``` + +### セキュリティ設定の確認 + +パケットフィルターの現在の設定を確認: +```bash +ruby scripts/utils/check_packet_filter.rb +``` + +期待される出力: +- ポート22, 80, 443が開放されている +- その他のポートは基本的にdeny + +## ベストプラクティス + +1. **両方の層を常に有効に保つ** + - パケットフィルターとiptablesの両方を有効化 + - 片方だけに依存しない + +2. **定期的な設定確認** + - パケットフィルターIDが有効か確認 + - スタートアップスクリプトが最新か確認 + +3. **テスト環境での検証** + - 本番環境に適用する前にテスト環境で検証 + - セキュリティ設定の変更は慎重に + +4. **ログの監視** + - 不正なアクセス試行を監視 + - 異常なトラフィックパターンを検知 + +## 関連ファイル + +- `scripts/sakura_server_user_agent.rb` - パケットフィルター適用ロジック +- `startup-scripts/112900928939` - iptables設定スクリプト +- `scripts/utils/check_packet_filter.rb` - パケットフィルター確認ツール +- `scripts/utils/check_server_status.rb` - サーバー状態確認ツール + +## 更新履歴 + +- 2019年9月: パケットフィルター機能を導入 +- 2025年7月: cloud-init対応でSSH鍵設定を改善 +- 2025年9月: 多層防御ドキュメント作成 + +--- + +*このドキュメントは、DojoPaaSのセキュリティ設計思想を説明するものです。実際の運用では、さくらインターネット様のセキュリティガイドラインも併せて参照してください。* \ No newline at end of file diff --git a/scripts/deploy.rb b/scripts/deploy.rb index 3ac4e97..739b0d2 100644 --- a/scripts/deploy.rb +++ b/scripts/deploy.rb @@ -70,6 +70,74 @@ def run() puts "the #{RESULT_INSTANCE_CSV} was saved!" end + # 個別サーバー作成メソッド(Rakeタスク用) + # DRY原則: 既存のロジックを最大限再利用 + def create_single_server(server_name) + # 初期化パラメータとAPIクライアントのセットアップ(既存ロジック再利用) + request_params = perform_init_params() + @ssua = SakuraServerUserAgent.new(**request_params) + @ssua.archive_id = initial_archive_id() + + # servers.csvから指定されたサーバー情報を取得 + server_info = nil + CSV.read(INSTANCE_CSV, headers: true).each do |line| + if line['name'] == server_name + server_info = line + break + end + end + + unless server_info + puts "❌ エラー: サーバー '#{server_name}' が servers.csv に見つかりません" + return false + end + + # 既存サーバーのチェック + puts '🔍 既存サーバーをチェック中...' + sakura_servers = (@ssua.get_servers())['Servers'] + sakura_servers.each do |s| + if s['Name'] == server_name + puts "⚠️ 警告: サーバー '#{server_name}' は既に存在します" + puts " IPアドレス: #{s['Interfaces'].first['IPAddress']}" + puts " 説明: #{s['Description']}" + return false + end + end + + # サーバー作成(既存のcreateメソッドを再利用) + puts "🚀 サーバー '#{server_name}' を作成中..." + puts " 説明: #{server_info['description']}" + puts " ブランチ: #{server_info['branch']}" + + begin + @ssua.create( + name: server_info['name'], + description: server_info['description'], + pubkey: server_info['pubkey'], + tag: server_info['branch'] + ) + + # 作成結果の確認 + sleep(5) # APIの反映待ち + result_servers = (@ssua.get_servers())['Servers'] + created_server = result_servers.find { |s| s['Name'] == server_name } + + if created_server + puts "✅ サーバー作成成功!" + puts " サーバー名: #{created_server['Name']}" + puts " IPアドレス: #{created_server['Interfaces'].first['IPAddress']}" + puts " 説明: #{created_server['Description']}" + return true + else + puts "❌ サーバー作成に失敗した可能性があります" + return false + end + rescue => e + puts "❌ エラー: #{e.message}" + return false + end + end + private diff --git a/scripts/initialize_server.rb b/scripts/initialize_server.rb index d1cadbc..74d94d9 100644 --- a/scripts/initialize_server.rb +++ b/scripts/initialize_server.rb @@ -55,13 +55,8 @@ def initialize(input, options = {}) @dry_run = options[:dry_run] || false @force = options[:force] || false - # さくらのクラウドAPIクライアント初期化(石狩第二ゾーン) - @ssua = SakuraServerUserAgent.new( - zone: "31002", - zone_id: "is1b", - packet_filter_id: nil, - verbose: @verbose - ) + # さくらのクラウドAPIクライアント初期化(デフォルト値使用) + @ssua = SakuraServerUserAgent.new(verbose: @verbose) end def run diff --git a/scripts/sakura_server_user_agent.rb b/scripts/sakura_server_user_agent.rb index 41bb756..7e8b04c 100644 --- a/scripts/sakura_server_user_agent.rb +++ b/scripts/sakura_server_user_agent.rb @@ -55,8 +55,9 @@ def self.normalize_ip_address(ip) end # jsのserver.createで使っているフィールドを参考 - def initialize(zone:0, packet_filter_id:nil, name:nil, description:nil, zone_id:"is1b", - tags:nil, pubkey:nil, resolve:nil, verbose:false) + # デフォルト値を本番環境(石狩第二)に設定 + def initialize(zone:"31002", packet_filter_id:'112900922505', name:nil, description:nil, zone_id:"is1b", + tags:nil, pubkey:nil, resolve:nil, verbose:false, notes:nil) @zone = zone @packet_filter_id = packet_filter_id @name = name @@ -65,8 +66,8 @@ def initialize(zone:0, packet_filter_id:nil, name:nil, description:nil, zone_id: @pubkey = pubkey @resolve = resolve @plan = 1001 # 1core 1Gb memory - # 標準スタートアップスクリプトを使用 - @notes = [{ID: STARTUP_SCRIPT_ID}] + # 標準スタートアップスクリプトを使用(デフォルト値または指定値) + @notes = notes || [{ID: STARTUP_SCRIPT_ID}] @sakura_zone_id = zone_id @archive_id = nil @verbose = verbose @@ -335,12 +336,16 @@ def _put_ssh_key(disk_id) end def _copying_image - # 通常版UbuntuではSSH鍵とスタートアップスクリプトIDは - # disk/config APIで既に設定済み(_put_ssh_keyメソッド内) - # ここではサーバーを起動するだけでよい + # SSH鍵はdisk/config APIで設定済み + # スタートアップスクリプトはサーバー起動時に指定する必要がある - puts "DEBUG: Starting server (SSH key and startup script already configured via disk/config API)" if @verbose - send_request('put',"server/#{@server_id}/power", nil) + puts "DEBUG: Starting server with startup script ID: #{@notes.first[:ID]}" if @verbose + + # サーバー起動時にスタートアップスクリプトIDを指定 + body = { + Notes: @notes + } + send_request('put',"server/#{@server_id}/power", body) rescue => exception puts exception diff --git a/scripts/utils/check_packet_filter.rb b/scripts/utils/check_packet_filter.rb new file mode 100755 index 0000000..95a2559 --- /dev/null +++ b/scripts/utils/check_packet_filter.rb @@ -0,0 +1,67 @@ +#!/usr/bin/env ruby + +require 'dotenv/load' +require_relative '../sakura_server_user_agent.rb' + +# パケットフィルターAPIにアクセスするための拡張クラス +class PacketFilterChecker < SakuraServerUserAgent + def get_packet_filter(id) + send_request('get', "packetfilter/#{id}", nil) + end + + def list_packet_filters + send_request('get', 'packetfilter', nil) + end +end + +puts "=== パケットフィルター情報確認 ===" +puts "" + +checker = PacketFilterChecker.new + +# デフォルトのパケットフィルターIDを確認 +default_id = '112900922505' +puts "📋 デフォルトパケットフィルター ID: #{default_id}" +puts "" + +begin + # 特定のパケットフィルター詳細を取得 + puts "詳細情報を取得中..." + filter_info = checker.get_packet_filter(default_id) + + if filter_info && filter_info['PacketFilter'] + pf = filter_info['PacketFilter'] + puts "名前: #{pf['Name']}" + puts "説明: #{pf['Description']}" + puts "" + + if pf['Expression'] && pf['Expression'].any? + puts "📝 ルール一覧:" + pf['Expression'].each_with_index do |rule, i| + puts " ルール#{i+1}:" + puts " - プロトコル: #{rule['Protocol']}" + puts " - 送信元: #{rule['SourceNetwork'] || 'any'}" + puts " - 送信元ポート: #{rule['SourcePort'] || 'any'}" + puts " - 宛先ポート: #{rule['DestinationPort'] || 'any'}" + puts " - アクション: #{rule['Action']}" + puts " - 説明: #{rule['Description']}" if rule['Description'] + puts "" + end + end + else + puts "❌ パケットフィルター情報を取得できませんでした" + end + + # 利用可能なパケットフィルター一覧 + puts "\n=== 利用可能なパケットフィルター一覧 ===" + all_filters = checker.list_packet_filters + if all_filters && all_filters['PacketFilters'] + all_filters['PacketFilters'].each do |pf| + puts "- ID: #{pf['ID']} / 名前: #{pf['Name']}" + end + end + +rescue => e + puts "エラー: #{e.message}" + puts e.backtrace if ENV['DEBUG'] +end \ No newline at end of file diff --git a/scripts/utils/check_server_status.rb b/scripts/utils/check_server_status.rb index 623a071..5d46922 100644 --- a/scripts/utils/check_server_status.rb +++ b/scripts/utils/check_server_status.rb @@ -11,13 +11,8 @@ search_term = ARGV[0] -params = { - zone: "31002", - zone_id: "is1b", - packet_filter_id: nil -} - -ssua = SakuraServerUserAgent.new(**params) +# デフォルト値を使用(石狩第二ゾーン) +ssua = SakuraServerUserAgent.new puts "=== サーバー詳細情報 ===" puts "検索条件: #{search_term}" diff --git a/scripts/utils/check_startup_scripts.rb b/scripts/utils/check_startup_scripts.rb index 8d6f0e1..4e05807 100755 --- a/scripts/utils/check_startup_scripts.rb +++ b/scripts/utils/check_startup_scripts.rb @@ -5,12 +5,6 @@ require 'dotenv/load' require_relative '../sakura_server_user_agent.rb' -params = { - zone: "31002", - zone_id: "is1b", - packet_filter_id: nil -} - puts "=== スタートアップスクリプト(Note)一覧 ===" puts "時刻: #{Time.now}" puts "" @@ -23,7 +17,8 @@ def get_notes end end - ssua = StartupScriptChecker.new(**params) + # デフォルト値を使用(石狩第二ゾーン) + ssua = StartupScriptChecker.new # Noteリソースの一覧を取得 response = ssua.get_notes() diff --git a/scripts/utils/find_resources.rb b/scripts/utils/find_resources.rb index 3d4a041..1599cfd 100755 --- a/scripts/utils/find_resources.rb +++ b/scripts/utils/find_resources.rb @@ -19,19 +19,13 @@ # 検索文字列(引数がなければ全て表示) search_term = ARGV[0] || "" -# 石狩第二ゾーンのパラメータ(本番環境) -params = { - zone: "31002", - zone_id: "is1b", - packet_filter_id: nil -} - puts "=== さくらのクラウド リソース検索 ===" puts "検索条件: #{search_term.empty? ? '全て' : search_term}" puts "時刻: #{Time.now.strftime('%Y-%m-%d %H:%M:%S')}" puts "" -ssua = SakuraServerUserAgent.new(**params) +# デフォルト値を使用(石狩第二ゾーン) +ssua = SakuraServerUserAgent.new # サーバーの確認 puts "【サーバー一覧】" diff --git a/scripts/utils/start_server.rb b/scripts/utils/start_server.rb index 17fff42..8a44637 100644 --- a/scripts/utils/start_server.rb +++ b/scripts/utils/start_server.rb @@ -15,13 +15,8 @@ server_id = ARGV[0] -params = { - zone: "31002", - zone_id: "is1b", - packet_filter_id: nil -} - -ssua = SakuraServerUserAgent.new(**params) +# デフォルト値を使用(石狩第二ゾーン) +ssua = SakuraServerUserAgent.new puts "サーバー #{server_id} を起動中..." diff --git a/test/README.md b/test/README.md new file mode 100644 index 0000000..6ae7b26 --- /dev/null +++ b/test/README.md @@ -0,0 +1,98 @@ +# テストディレクトリ構成 + +## ディレクトリ構造 + +``` +test/ +├── README.md # このファイル +├── csv_test.rb # CSVフォーマットのバリデーションテスト +├── ip_validation_test.rb # IPアドレス検証のテスト +└── integration/ # 統合テスト・実際のAPI呼び出しテスト + ├── test_regex_patterns.rb # 正規表現パターンのテスト + ├── test_server_with_notes.rb # スタートアップスクリプト実行テスト + ├── test_with_packet_filter.rb # パケットフィルター適用テスト + └── test_with_startup_script.rb # スタートアップスクリプトテスト +``` + +## テストの種類 + +### ユニットテスト(`test/`直下) + +- **csv_test.rb**: `servers.csv`のフォーマットを検証 + - ヘッダーの存在確認 + - 必須フィールドのチェック + - SSH公開鍵の形式検証 + +- **ip_validation_test.rb**: IPアドレス検証機能のテスト + - 有効/無効なIPアドレスのテストケース + - 正規化処理のテスト + +### 統合テスト(`test/integration/`) + +実際のさくらのクラウドAPIと連携するテスト。**本番環境では実行しないでください。** + +- **test_with_packet_filter.rb**: 本番環境設定でのサーバー作成テスト + - パケットフィルター(ファイアウォール)の適用確認 + - ポート22, 80, 443の開放確認 + +- **test_with_startup_script.rb**: スタートアップスクリプトのテスト + - スクリプトID: 112900928939の実行確認 + - iptables設定、SSH強化設定の適用確認 + +- **test_server_with_notes.rb**: Notes APIフィールドのデバッグ + - disk/config APIでのNotes設定動作確認 + - サーバー起動時のNotes指定確認 + +- **test_regex_patterns.rb**: Issue解析用正規表現のテスト + - CoderDojo名の抽出パターン + - IPアドレスの抽出パターン + +## テストの実行方法 + +### ユニットテスト + +```bash +# CSVフォーマットテスト +bundle exec ruby test/csv_test.rb + +# IPアドレス検証テスト +bundle exec ruby test/ip_validation_test.rb + +# すべてのユニットテスト +bundle exec rake test +``` + +### 統合テスト + +⚠️ **警告**: これらのテストは実際のリソースを作成する可能性があります。 + +```bash +# 環境変数の設定が必要 +export SACLOUD_ACCESS_TOKEN=xxxx +export SACLOUD_ACCESS_TOKEN_SECRET=xxxx +export SSH_PUBLIC_KEY_PATH=~/.ssh/id_rsa.pub + +# 個別実行(テスト環境のみ) +ruby test/integration/test_with_packet_filter.rb test-server +ruby test/integration/test_with_startup_script.rb test-server +``` + +## 注意事項 + +1. **統合テストは本番環境で実行しない** + - 実際のサーバーが作成される + - 課金が発生する可能性がある + +2. **API認証情報が必要** + - `SACLOUD_ACCESS_TOKEN` + - `SACLOUD_ACCESS_TOKEN_SECRET` + +3. **SSH公開鍵が必要** + - デフォルト: `~/.ssh/id_rsa.pub` + - 環境変数: `SSH_PUBLIC_KEY_PATH` + +## 関連ドキュメント + +- [セキュリティ設計](../docs/security-defense-in-depth.md) +- [サーバー初期化手順](../docs/initialize-server.md) +- [SSH接続設定](../docs/ssh.md) \ No newline at end of file diff --git a/scripts/utils/test_regex_patterns.rb b/test/integration/test_regex_patterns.rb similarity index 100% rename from scripts/utils/test_regex_patterns.rb rename to test/integration/test_regex_patterns.rb diff --git a/test/integration/test_server_with_notes.rb b/test/integration/test_server_with_notes.rb new file mode 100755 index 0000000..78caba0 --- /dev/null +++ b/test/integration/test_server_with_notes.rb @@ -0,0 +1,95 @@ +#!/usr/bin/env ruby + +# スタートアップスクリプトが正しく実行されるかテスト +# 通常版Ubuntuでdisk/config APIのNotesフィールドが機能するか確認 + +require 'dotenv/load' +require_relative '../../scripts/sakura_server_user_agent.rb' + +if ARGV.length < 1 + puts "使用方法: ruby #{$0} <サーバー名>" + puts "例: ruby #{$0} test-startup-script" + exit 1 +end + +server_name = ARGV[0] +ssh_key_path = ENV['SSH_PUBLIC_KEY_PATH'] || File.expand_path('~/.ssh/id_rsa.pub') + +unless File.exist?(ssh_key_path) + puts "SSH公開鍵が見つかりません: #{ssh_key_path}" + exit 1 +end + +pubkey = File.read(ssh_key_path).strip + +puts "=== スタートアップスクリプトのテスト ===" +puts "サーバー名: #{server_name}" +puts "スタートアップスクリプトID: #{SakuraServerUserAgent::STARTUP_SCRIPT_ID}" +puts "" + +# disk/config APIでのNotes設定をデバッグ +class DebugServerUserAgent < SakuraServerUserAgent + def test_disk_config(disk_id, pubkey) + body = { + SSHKey: { + PublicKey: pubkey + }, + Notes: [{ID: STARTUP_SCRIPT_ID}] # ここがポイント + } + + puts "📋 disk/config APIに送信するデータ:" + puts JSON.pretty_generate(body) + puts "" + + # 実際にAPIを呼び出す + response = send_request('put',"disk/#{disk_id}/config", body) + + puts "📋 APIレスポンス:" + puts JSON.pretty_generate(response) if response + + response + end + + # サーバー起動時のパラメータも確認 + def test_server_start(server_id, with_notes = false) + if with_notes + # Notesをサーバー起動時に指定する方法(テスト) + body = { + Notes: [{ID: STARTUP_SCRIPT_ID}] + } + puts "📋 サーバー起動時にNotesを指定:" + puts JSON.pretty_generate(body) + response = send_request('put',"server/#{server_id}/power", body) + else + # 現在の実装(Notesなし) + puts "📋 サーバー起動時にNotesを指定しない(現在の実装)" + response = send_request('put',"server/#{server_id}/power", nil) + end + + response + end +end + +# APIドキュメントの確認事項を出力 +puts "⚠️ 確認事項:" +puts "1. disk/config APIのNotesフィールドはスタートアップスクリプトIDを受け付けるか?" +puts "2. それとも、サーバー起動時(/power API)にNotesを指定する必要があるか?" +puts "3. 通常版UbuntuとCloudImg版で動作が異なるか?" +puts "" +puts "参考: さくらのクラウドAPIドキュメント" +puts " https://manual.sakura.ad.jp/cloud/api/1.1/disk.html#put-disk-disk_id-config" +puts " https://manual.sakura.ad.jp/cloud/api/1.1/server.html#put-server-server_id-power" +puts "" + +# 実際のテストは危険なのでコメントアウト +# agent = DebugServerUserAgent.new(verbose: true) +# agent.create(name: server_name, description: "Test startup script", pubkey: pubkey, tag: "test") + +puts "📝 現在の実装の問題点:" +puts "- disk/config APIでNotesを設定しているが、これがスタートアップスクリプトとして実行されるか不明" +puts "- cloud-init削除時にスタートアップスクリプトの実行方法も削除された可能性" +puts "" +puts "📝 解決策の候補:" +puts "1. サーバー起動時(/power API)にNotesパラメータを追加" +puts "2. 別のAPIエンドポイントでスタートアップスクリプトを設定" +puts "3. cloud-initを部分的に復活(スタートアップスクリプト実行のみ)" \ No newline at end of file diff --git a/scripts/utils/test_with_packet_filter.rb b/test/integration/test_with_packet_filter.rb similarity index 89% rename from scripts/utils/test_with_packet_filter.rb rename to test/integration/test_with_packet_filter.rb index f6615ab..0d3aea1 100755 --- a/scripts/utils/test_with_packet_filter.rb +++ b/test/integration/test_with_packet_filter.rb @@ -4,7 +4,7 @@ # 通常版Ubuntu 24.04 + disk/config API + @notes + パケットフィルター require 'dotenv/load' -require_relative '../sakura_server_user_agent.rb' +require_relative '../../scripts/sakura_server_user_agent.rb' # SSH公開鍵を読み込み ssh_public_key_path = ENV['SSH_PUBLIC_KEY_PATH'] || File.expand_path('~/.ssh/id_rsa.pub') @@ -20,13 +20,6 @@ puts "サーバー名: #{server_name}" puts "" -# 本番環境と同じパラメータ -params = { - zone: "31002", # 石狩第二 - zone_id: "is1b", # 石狩第二 - packet_filter_id: '112900922505', # 本番環境のパケットフィルターID -} - puts "📋 設定内容:" puts " - ゾーン: 石狩第二 (is1b)" puts " - パケットフィルターID: 112900922505" @@ -34,7 +27,8 @@ puts "" begin - ssua = SakuraServerUserAgent.new(**params) + # デフォルト値を使用(パケットフィルター含む) + ssua = SakuraServerUserAgent.new # 通常版Ubuntu 24.04を検索 archives = ssua.get_archives()['Archives'] diff --git a/scripts/utils/test_with_startup_script.rb b/test/integration/test_with_startup_script.rb similarity index 90% rename from scripts/utils/test_with_startup_script.rb rename to test/integration/test_with_startup_script.rb index e82d7dd..f137f99 100755 --- a/scripts/utils/test_with_startup_script.rb +++ b/test/integration/test_with_startup_script.rb @@ -4,7 +4,7 @@ # デフォルトでSTARTUP_SCRIPT_IDが設定される require 'dotenv/load' -require_relative '../sakura_server_user_agent.rb' +require_relative '../../scripts/sakura_server_user_agent.rb' # SSH公開鍵を読み込み ssh_public_key_path = ENV['SSH_PUBLIC_KEY_PATH'] || File.expand_path('~/.ssh/id_rsa.pub') @@ -21,15 +21,9 @@ puts "スタートアップスクリプトID: #{SakuraServerUserAgent::STARTUP_SCRIPT_ID}" puts "" -# サーバー作成パラメータ(シンプル) -params = { - zone: "31002", - zone_id: "is1b", - packet_filter_id: nil, # テスト用にパケットフィルター無効 -} - begin - ssua = SakuraServerUserAgent.new(**params) + # デフォルト値を使用(テスト用にパケットフィルター無効) + ssua = SakuraServerUserAgent.new(packet_filter_id: nil) # 通常版Ubuntu 24.04を検索 archives = ssua.get_archives()['Archives']