本記事について
本記事は、/etc/sysconfig/iptables
にドメイン名を直接書いたものを、iptablesに定期的に反映させる方法について私見を述べたものです。
環境
Amazon Linux 2
Python3
背景
iptablesの罠
以下のように、iptablesをドメイン名で記載している箇所がある。
変更する際はsudo vi /etc/sysconfig/iptables
コマンドで直接編集しsudo service iptables restart
で反映させる運用となっている。
-A OUTPUT -p tcp -d dev.**.***.net -m owner --uid-owner userName -j ACCEPT
-A OUTPUT -p tcp -d prd.**.***.net -m owner --uid-owner userName -j ACCEPT
-A OUTPUT -p tcp -d ************.**.net -m owner --uid-owner userName -j ACCEPT
上記のように書くと一見、DNSのAレコードの向き先を宛先とするパケットをACCEPTするかのように見える。
しかし実際には、iptablesサービスを起動したタイミングと、iptables設定を「反映」したタイミングでしかDNS解決が行われない。
問題点
目的としては、上記設定に記載されたサーバへSSHを行う。
サービス再起動などでiptables設定が反映された直後は、DNSが解決されて間もないため、上記設定は有効なものであり、正常にSSH接続を行うことができる。
しかし時間経過やその他何らかの要因により相手サーバのIPアドレスが変わってしまうと・・・
その後接続する際にSSHコマンド側でDNS解決されたIPアドレスと、iptablesに既に読み込まれたIPアドレスが食い違うため接続が出来なくなる。
都度sudo service iptables restart
すりゃいいという話かもしれないが、当該端末を使用するすべてのユーザがこのコマンドを実行できるとは限らない。
そのためSSH接続が出来る、という仕様をそうしたユーザに提供できないという問題があった。
方針
- きたなくてもいいので、とにかく速く実装する。着手したその日に終わらす。
-
/etc/sysconfig/iptables
ファイルを原本とする。 - 3時間ごとに下記処理を行う
-
/etc/sysconfig/iptables
ファイルを読み込み、ドメイン名の文字列を抽出する。 - 上記ドメイン名を名前解決し、AレコードのIPアドレスを取得する。
- 前回(3時間前)の取得結果と比較し、IPアドレスの変わったものがあればiptablesを更新する。差分はテキストファイルに保存する。
- 取得結果をテキストファイルに保存し、次回実行時に用いる。
-
- ドメイン名を正規表現で指定し、該当するドメイン名については処理を行わない
実装
概要
pydig
モジュールを用いて名前解決を行う。それ以外はPythonのbultin functionと標準モジュールでベタ書きする。
※要pip install
pip install pydig
ディレクトリ構成
- /root/python ・・・ソースファイルを格納
- conf ・・・設定ファイルを格納
- log ・・・ログファイル(?)を格納
- tmp ・・・とりあえず置き場所に困ったもので処理によって生成されるファイル、ならびに一時ファイルを格納
- venv ・・・
python3 -m venv venv
の結果が格納されている
環境構築
cd /root/python
./venv/bin/python3 dig_refreshIptables.py
cronからPythonを呼ぶためだけのシェルスクリプト
import os
import shutil
import re
import subprocess
from datetime import datetime as dt
import pydig
RAW_IPTABLES = '/etc/sysconfig/iptables'
DIG_RESULT_FILE = './tmp/iptables_past_dig_result.txt'
DIG_RESULT_FILE_BAK = lambda x: f'./tmp/iptables_past_dig_result_{x}.txt'
LOG_FILE = f'./log/iptables_dig_diff.log'
EXCLUDE_CONF = f'./conf/iptables_dig_exclude_pattern.txt'
def dig(name):
result = pydig.query(name, 'A')
return result[-1]
shutil.copy(RAW_IPTABLES, './tmp')
with open('./tmp/iptables', mode='r') as f:
lines = f.readlines()
# iptables記述からアドレス指定を抽出
addrs = []
for line in lines:
recs = line.split(' ')
if line.startswith('#') or len(recs) == 0:
continue
for i, rec in enumerate(recs):
if rec == '-d':
addr = recs[i + 1]
addrs.append(addr)
break
def checkRegex(pattern, content):
m = re.match(pattern, content)
if m is None:
return False
return True
# アドレス指定からDNS文字列を抽出
names = []
for addr in addrs:
if checkRegex('.*?[a-zA-Z].*?', addr):
names.append(addr)
# dig実行
digResult = {name: dig(name) for name in names}
def makeDigResultFile(digResult):
with open(DIG_RESULT_FILE, mode='w', encoding='utf-8') as f:
f.write('name\tinA\n')
for k, v in digResult.items():
f.write(f'{k}\t{v}')
f.write('\n')
if not os.path.exists(DIG_RESULT_FILE):
print('first exec, exiting...')
makeDigResultFile(digResult)
exit(0)
def readDigResultFile():
with open(DIG_RESULT_FILE, mode='r', encoding='utf-8') as f:
i = 0
result = {}
for line in f.readlines():
i += 1
if i == 1:
continue
rec = line.split('\t')
rec = [x.strip() for x in rec]
result[rec[0]] = rec[1]
return result
def loadExcludeConfig():
with open(EXCLUDE_CONF, mode='r') as f:
return [x.strip() for x in f.readlines()]
# print('reading dig result file...')
old = readDigResultFile()
excludes = loadExcludeConfig()
difference = []
needRefresh = False
for k, v in digResult.items():
isExcluded = False
for ex in excludes:
if checkRegex(ex, k):
isExcluded = True
break
if isExcluded:
continue
if k not in old:
continue
if v != old[k]:
difference.append(f'{k}\t{v}\t{old[k]}')
needRefresh = True
ts = dt.now().strftime('%Y%m%d_%H%M%S')
# shutil.copy(DIG_RESULT_FILE, DIG_RESULT_FILE_BAK(ts))
makeDigResultFile(digResult)
with open(LOG_FILE, mode='a', encoding='utf-8') as f:
for d in difference:
f.write(f'{ts}\t{d}')
f.write('\n')
def refreshIptables():
print(f'{ts}\trefreshing iptables...')
subprocess.run(['./_refreshIptables.sh'], shell=True)
print(f'iptables refresh has finished!!!')
if needRefresh:
refreshIptables()
メインの処理。
処理内容は前述
sudo iptables-restore < /etc/sysconfig/iptables
iptablesを更新するコマンドを記載。Pythonからsubprocessモジュールを用いて呼び出している。
.*?\.elb\..*?
処理を行わないドメイン名を正規表現で指定。(改行区切りで複数指定することができる。)
正規表現はPythonのre
モジュールの正規表現を用いる。
この例では、.elb.
を含むドメイン名は本処理の対象としない、ということを示している。
定期実行の設定
crontabの設定を行う。
# crontab -e
viエディタが開くので、以下を追記する。
0 */3 * * * bash /root/python/dig_refreshIptables.py
tail -f /var/log/cron
コマンドを用いることで、設定が行われたことを確認できる。
条件をみたす時刻まで待つと、上記コマンドが実行された旨が表示される。
(ちなみに動作確認だけしたければ* * * * *
として毎分実行すればいい)
Feb 17 20:37:14 ****** crontab[******]: (root) END EDIT (root)
Feb 17 20:37:42 ****** crontab[******]: (root) BEGIN EDIT (root)
:
:
Feb 17 21:00:02 ****** CROND[******]: (root) CMD (bash /root/python/dig_refreshIptables.py)
結果
上記cron実行後、相手サーバへ正常にSSHすることが出来た。
[userName@****** ~]$ ssh -i key.pem user@dev.**.***.net
Last login: *** *** ** **:**:** 2022 from ip-**-**-**-***.ap-northeast-1.compute.internal
__| __|_ )
_| ( / Amazon Linux 2 AMI
___|\___|___|
https://aws.amazon.com/amazon-linux-2/
[user@ip-**-**-**-*** ~]$