はじめに

LinuxClubではスポーツ大会のエントリーサイトを運営しています。今年も体育会からの要請があり運営を行いました。この記事では運用に際し改善、改良を行った点を紹介します。

DNS

従来は www2.linux.it.teu.ac.jp/sports20xx/ の下に設置していました。今年度のサイトはサブドメイン sports.linux.it.teu.ac.jp に設置しました。

Nginx

担当: panakuma

ページ読み込みの高速化によるユーザビリティとセキュリティの向上のためHTTP/2とTLSv1.3に対応させました。

TLSv1.3対応

TLSv1.3はまだドラフト段階の規格なので5月現在で最新であったdraft22/23に対応しました。

使用したプログラム

  • Nginx 1.13.10
  • OpenSSL 1.1.1-pre3

それぞれソースを公式サイトからダウンロードし、OpenSSLを組み込んでNginxをmakeしました。

レスポンスヘッダ/Cookie属性の変更

担当: koyama

セキュリティを高めるためにレスポンスヘッダへ以下を付与しました。

  • strict-transport-security: max-age=15768000; includeSubdomains
  • x-frame-options: SAMEORIGIN
  • x-xss-protection: 1; mode=block
  • x-content-type-options: nosniff
  • cache-control: private, no-store
  • pragma: no-cache
$ curl -si https://sports.linux.it.teu.ac.jp/ | head -n 15
HTTP/2 200
server: nginx
date: Wed, 13 Jun 2018 01:36:00 GMT
content-type: text/html; charset=utf-8
content-length: 2413
strict-transport-security: max-age=15768000; includeSubDomains
x-frame-options: SAMEORIGIN
x-xss-protection: 1; mode=block
x-content-type-options: nosniff
cache-control: private, no-store
pragma: no-cache

<!doctype html>

<html>

それぞれのヘッダの役割は以下です。

  • strict-transport-security はhttpsへ強制リダイレクトを実現しています。
  • x-frame-options はクリックジャッキング対策で付与しています。
  • xss-protection はブラウザのXSSフィルターを強制的にONに設定します。
  • x-content-type-options はContent-Typeの誤認識によるXSSを回避するために付与しています。
  • cache-control はプロキシサーバなどで機密情報が保存されないために付与しています。
  • program は古いブラウザでのキャッシュ保持を防ぎます。

ブラウザのCookieに secure 属性と httponly 属性を付与しました。 secure 属性はクッキーをhttpsでやり取りするために付与します。 httponly 属性はJavaScriptからのCookieへのアクセスを禁止します。

セッションのタイムアウトを実装するためにCookieの expires 属性へセッション期限を設定しています。

これらはフレームワークの機能を使っています。そのため、セッションタイムの実装などを直接書かずに実現しています。

ソースコードの修正(サーバサイド)

担当: koyama

既存のPythonコードを修正することで、チーム数制限を実装しました。従来はエントリー可能なチーム数の上限を設定していなかった為、上限なくエントリーできてしまいました。今年度のシステムはエントリー済みチーム数を取得することによって、エントリー上限に達するとエントリができなくなる仕組みを作りました。

ソースコードの修正(フロント)

担当: homirun

デザインの変更

例年ページのテーマカラーが変わっていたため今年も気分で変えてみました。 また競技のルールを表示するページをアコーディオンボタンで開けるようにしました。

let check = 0
$('.expander').click(function() {
	if (check === 0){
		$(this).closest('article').children('aside').show(300);
		$(this).children('i').removeClass('fa-angle-down')
		$(this).children('i').addClass('fa-angle-up')
		check = 1
	}else{
		$(this).closest('article').children('aside').hide(300);
		$(this).children('i').removeClass('fa-angle-up')
		$(this).children('i').addClass('fa-angle-down')
		check = 0
	}
});

checkで現在のメニューの開閉状態を保持しています。 checkが0のときはarticleの子要素を表示させ、1のときは非表示にさせてます。

そのほかFont Awesomeを用いボタンのアイコンなどわかりやすいものに変更しました。

 

UI崩れ

モバイル端末でトップページを開いた際に画像がはみ出してしまう事があったのでCSSの該当箇所にmax-width: 100%を追記し修正しました。

脆弱性診断を実施

担当: koyama

オープンソースの脆弱性診断ツールである OWASP ZAPBurpSuite(Community Edition)を使用しました。

まず、 OWASP ZAP のスキャン機能を使って既知な脆弱性を探しました。指摘された事項としては、レスポンスヘッダに Progma ヘッダが未付与であることだけでした。

次に、 BurpSuite (ローカルプロキシ)を使って手動で診断します。ブラウザを起動してプロキシの設定を localhost:8080 に設定します。BurpSuiteのProxyタブ内からIntereptをOffにしておきます。診断は以下の手順で行います。

1.ブラウザを起動してWebサイトにアクセスします。

2.Proxyタブ内からHTTP Proxyを選ぶとリクエスト一覧が表示されます。

3.リクエスト一覧から診断したいリクエストを見つけて、右クリック -> Send to Repeater を実行します。

4.Repeaterタブを開いてリクエストを書き換えて Go をクリックします。右側にレスポンス結果が表示されるので、この結果から脆弱性の有無を判断します。

以下は診断過程の一部です。クロスサイトスクリプティングの脆弱性がないか確認をしています。

運用

担当: koyama

問い合わせ先はGoogle Formを使用しました。また、Google Formが送信されるとSlackへ通知されるようプラグインを設定しました。

サイト監視用スクリプトを作成しました。スクリプトは実行されるとcurlコマンドを実行してステータスコードを確認します。ステータスコードが400未満であれば正常であると判断します。それ以外の場合は異常であると判断してSlackに通知を送信します。

#!/bin/bash

URL="https://sports.linux.it.teu.ac.jp/"
DEBUG=1
DATE=$(date "+%Y/%m/%d %H:%M:%S")

# Send Req
RESPONSE_CODE=$(curl -LI $URL -o /dev/null -w '%{http_code}\n' -s)

# Handle ReqCode
if [ $RESPONSE_CODE -lt 500 ] ; then # RESPONSE_CODE < 500

  # SUCCESS
  test $DEBUG -gt 0 && echo "[DEBUG] Successful access"

else
  
  # FAIL
  test $DEBUG -gt 0 && echo "[DEBUG] Fail to access"

  ### Post to Slack
  TOKEN=''
  USER='SiteChecker'
  CHANNEL='sportsfes'
  MESSAGE="[Error:$RESPONSE_CODE] Fail to access $URL at $DATE"

  curl -s -XPOST -d "token=$TOKEN" \
                 -d "channel=#$CHANNEL" \
                 -d "text=$MESSAGE" \
                 -d "username=$USER" \
                 "https://slack.com/api/chat.postMessage" # >& /dev/null &

fi

Nginxのログ解析用のスクリプトをPythonで作成しました。

#!/usr/bin/env python

def log2dict(logfile):
	with open(logfile, "r") as file:
		file_content = file.read()

	lines = [ file_content.split('\n') ][0]
	formated_logs = []

	for tmp in lines:
		try:
			ip_date = tmp.split('"')[0]
			ipaddr = ip_date.split()[0]
			date = ip_date.split()[3][1:]

			method_path_ver = tmp.split('"')[1]
			method = method_path_ver.split()[0]
			path = method_path_ver.split()[1]
			ver = method_path_ver.split()[2]

			referer = tmp.split('"')[3]
			useragent = tmp.split('"')[5]

		except:
			continue

		formated_logs.append({
			'ipaddr'   : ipaddr,
			'date'     : date,
			'method'   : method,
			'path'     : path,
			'version'  : ver,
			'referer'  : referer,
			'useragent': useragent
		})

	return formated_logs


if __name__ == '__main__':
	dict_logs = log2dict(logfile="sports-access.log")

	# Get all ipaddrs
	ipaddrs = [ log['ipaddr'] for log in dict_logs ]

	# Get total
	# Python3) often_ipaddrs = { ip: ipaddrs.count(ip) for ip in set(ipaddrs) }
	often_ipaddrs = {}
	for ip in set(ipaddrs):
		often_ipaddrs[ip] = ipaddrs.count(ip)

	# Print ranking
	for k_ip, v_often in sorted(often_ipaddrs.items(), key=lambda x:x[1], reverse=True):
		print  v_often, k_ip