インターネット接続が禁止されているLinuxのアップデートについて

最近インターネットに繋がらないLinuxRHEL)サーバの脆弱性対応を行う機会があった。検証環境もインターネットに繋がらないので、レッドハット社の脆弱性公開ページから該当する更新パッケージを調べて、ローカル端末にダウンロードし、rpmコマンドでインストールしてみて依存パッケージが見つかったら、またブラウザから落としてきて…、という作業をするハメになった。

Vulnerability Responses - Red Hat Customer Portal

本来ならyumコマンド一発でアップデートできるところを、シコシコ手動アップデートしたおかげで検証だけで3日かかった。非常に疲れた。

現在開発要員と保守要員を兼務しているので、『セキュリティパッチ対応に工数を割いている時間などないのでは』という上司の懸念の声もあり、結果、「う~ん、この脆弱性は影響度:低!w じゃけん次回リリースに持ち越しましょうね~」という説明資料を書くことになった。しかしシステム監査の人はなんだかワイのことがキライみたいで、いつもいつも「念のために当てといてください」と無愛想に言いばかりで、開発スケジュールはチエンチエンなのだった。

本題

今回の経験から、「自分が設計をする立場になったら、アップデート用のProxyサーバは絶対に検討に入れよう」と強く思ったので、Proxyを通してyumからパッケージをダウンロードする方法について調べた。

検証のため、Proxy用のサーバと、Client用のサーバを用意した。
OSは両方CentOS7.5、Proxyにはオープンソースsquidを使う。

要件としては、Clientサーバはインターネットに接続できないネットワークに配置し、Proxyを通してyumによるパッケージダウンロードができるようにする。Proxyはインターネットに接続できるネットワークに配置し、ホワイトリストでパッケージダウンロードに必要なドメインにのみ接続できるようにする。

Clientサーバ側の設定

/etc/profile.d/proxy.shというファイルを作成し、環境変数にプロキシの接続情報を設定する。

===
/etc/profile.d/proxy.sh
===

PROXY="192.168.1.5:3128"
export http_proxy="http://$PROXY/"
export https_proxy="https://$PROXY/"
export HTTP_PROXY="http://$PROXY/"
export HTTPS_PROXY="https://$PROXY/"

ちなみに上記の設定だとcurlなど他のコマンドもProxyに接続するようになる。yumだけがProxyを使えるようにするなら/etc/yum.confを設定する。

次にyumの接続先となるリポジトリサーバを変更する。デフォルトでは特定のリポジトリにアクセスするわけではなく、ミラーリスト内で最もレスポンスが早かったリポジトリからパッケージを落とすようになっている。これではProxy側でアクセス先ドメインを制限することができないので、特定のリポジトリにのみアクセスするようにする。

===
/etc/yum.repos.d/CentOS-Base.repo
===

# CentOS-Base.repo
#
# The mirror system uses the connecting IP address of the client and the
# update status of each mirror to pick mirrors that are updated to and
# geographically close to the client.  You should use this for CentOS updates
# unless you are manually picking other mirrors.
#
# If the mirrorlist= does not work for you, as a fall back you can try the
# remarked out baseurl= line instead.
#
#

[base]
name=CentOS-$releasever - Base
#mirrorlist=http://mirrorlist.centos.org/?release=$releasever&arch=$basearch&repo=os&infra=$infra
baseurl=http://ftp.jaist.ac.jp/pub/Linux/CentOS/$releasever/os/$basearch/
        http://ftp.iij.ad.jp/pub/linux/centos/$releasever/os/$basearch/
        http://ftp.riken.jp/Linux/centos/$releasever/os/$basearch/
        http://ftp.nara.wide.ad.jp/pub/Linux/centos/$releasever/os/$basearch/
gpgcheck=1
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-7

#released updates
[updates]
name=CentOS-$releasever - Updates
#mirrorlist=http://mirrorlist.centos.org/?release=$releasever&arch=$basearch&repo=updates&infra=$infra
baseurl=http://ftp.jaist.ac.jp/pub/Linux/CentOS/$releasever/updates/$basearch/
        http://ftp.iij.ad.jp/pub/linux/centos/$releasever/updates/$basearch/
        http://ftp.riken.jp/Linux/centos/$releasever/updates/$basearch/
        http://ftp.nara.wide.ad.jp/pub/Linux/centos/$releasever/updates/$basearch/
gpgcheck=1
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-7

#additional packages that may be useful
[extras]
name=CentOS-$releasever - Extras
#mirrorlist=http://mirrorlist.centos.org/?release=$releasever&arch=$basearch&repo=extras&infra=$infra
baseurl=http://ftp.jaist.ac.jp/pub/Linux/CentOS/$releasever/extras/$basearch/
        http://ftp.iij.ad.jp/pub/linux/centos/$releasever/extras/$basearch/
        http://ftp.riken.jp/Linux/centos/$releasever/extras/$basearch/
        http://ftp.nara.wide.ad.jp/pub/Linux/centos/$releasever/extras/$basearch/
gpgcheck=1
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-7

#additional packages that extend functionality of existing packages
[centosplus]
name=CentOS-$releasever - Plus
#mirrorlist=http://mirrorlist.centos.org/?release=$releasever&arch=$basearch&repo=centosplus&infra=$infra
baseurl=http://ftp.jaist.ac.jp/pub/Linux/CentOS/$releasever/centosplus/$basearch/
        http://ftp.iij.ad.jp/pub/linux/centos/$releasever/centosplus/$basearch/
        http://ftp.riken.jp/Linux/centos/$releasever/centosplus/$basearch/
        http://ftp.nara.wide.ad.jp/pub/Linux/centos/$releasever/centosplus/$basearch/
gpgcheck=1
enabled=0
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-7

上記の設定ファイルで下記の4つの日本ドメインリポジトリにアクセスするように設定している。1ドメインしか設定していないと、そのリポジトリにアクセスできなかった場合にyum自体が失敗してしまうので、複数接続先を設定しておいたほうがいいだろう。

Proxyサーバ側の設定

squidをインストールする。

# yum install -y squid
# systemctl enable squid
# systemctl start squid
# systemctl stop firewalld //firewallは停止しておく

設定ファイルを書き換える。下記設定ではsquidは3128番ポートで待ち受け、192.168.0.0/16ネットワークからホワイトリストで設定されたドメイン群の80番、443番ポートへのアクセスのみ中継するようになっている。

===
/etc/squid/squid.conf
===

#
# Recommended minimum configuration:
#

# Example rule allowing access from your local networks.
# Adapt to list your (internal) IP networks from where browsing
# should be allowed
acl localnet src 192.168.0.0/16 # RFC1918 possible internal network

acl SSL_ports port 443
acl Safe_ports port 80          # http
acl Safe_ports port 443         # https
acl CONNECT method CONNECT

acl whitelist dstdomain "/etc/squid/whitelist"

# Deny requests to certain unsafe ports
http_access deny !Safe_ports

# Deny CONNECT to other than secure SSL ports
http_access deny CONNECT !SSL_ports

# Only allow cachemgr access from localhost
http_access allow localhost manager
http_access deny manager

# INSERT YOUR OWN RULE(S) HERE TO ALLOW ACCESS FROM YOUR CLIENTS

http_access allow localhost
http_access allow localnet whitelist

# And finally deny all other access to this proxy
http_access deny all

# Squid normally listens to port 3128
http_port 3128

# Uncomment and adjust the following to add a disk cache directory.
#cache_dir ufs /var/spool/squid 100 16 256

# Leave coredumps in the first cache dir
coredump_dir /var/spool/squid

#
# Add any of your own refresh_pattern entries above these.
#
refresh_pattern ^ftp:           1440    20%     10080
refresh_pattern ^gopher:        1440    0%      1440
refresh_pattern -i (/cgi-bin/|\?) 0     0%      0
refresh_pattern .               0       20%     4320

ホワイトリストに設定するのはClientサーバで設定したyumの接続先リポジトリドメインである。

===
/etc/squid/whitelist
===

ftp.jaist.ac.jp
ftp.iij.ad.jp
ftp.riken.jp
ftp.nara.wide.ad.jp

設定を読み込むためsquidをリロードする。

# systemctl reload squid
動作確認

Clientからyumでパッケージが入れられることを確認する。

# yum install wget
読み込んだプラグイン:fastestmirror
Loading mirror speeds from cached hostfile
 * base: ftp.iij.ad.jp
 * extras: ftp.iij.ad.jp
 * updates: ftp.iij.ad.jp
依存性の解決をしています
--> トランザクションの確認を実行しています。
---> パッケージ wget.x86_64 0:1.14-15.el7_4.1 を インストール
--> 依存性解決を終了しました。

依存性を解決しました

=====================================================================================================================================
 Package                     アーキテクチャー              バージョン                              リポジトリー                 容量
=====================================================================================================================================
インストール中:
 wget                        x86_64                        1.14-15.el7_4.1                         base                        547 k

トランザクションの要約
=====================================================================================================================================
インストール  1 パッケージ

総ダウンロード容量: 547 k
インストール容量: 2.0 M
Is this ok [y/d/N]: y
Downloading packages:
wget-1.14-15.el7_4.1.x86_64.rpm                                                                               | 547 kB  00:00:00
Running transaction check
Running transaction test
Transaction test succeeded
Running transaction
  インストール中          : wget-1.14-15.el7_4.1.x86_64                                                                          1/1
  検証中                  : wget-1.14-15.el7_4.1.x86_64                                                                          1/1

インストール:
  wget.x86_64 0:1.14-15.el7_4.1

完了しました!

Proxy経由でパッケージをダウンロードすることができた。

所感

パッケージアップデートにProxyを用意するのは基本的な内容だと思うが、今までセキュリティパッチ周りの運用を考える機会がなかったため、改めて勉強する良い機会になった。今後はWindowsや他のディストリビューションではどのような設定が必要かも調査していきたい。

fluentdのfluent-plugin-kinesisプラグインを使って、Kinesis Firehoseにログを送信する。

最近、自分が関わっているような新しい技術の導入に保守的なプロジェクトでも、「AWSのAutoScalingを使いたい」というような話が浮上するようになった。

自動的にサーバが増減するシステムとなると、各サーバに保存しているログをバッチ処理で収集するなんて方法では対応ができなくなってしまう。ログをリアルタイムに収集し続ける仕組みが必要なわけだが、かといってsyslogはログの出力元のアプリケーションを色々変更する必要があり面倒くさい。

そうなると、今流行り(というかすでに成熟期な気もするが)のfluentdやLogStashを勉強する必要があるのだろう。また、最近はログ収集サーバを立てるのではなく、収集から分析までクラウドのマネジメントサービスを使うのがクールな風潮だ。

そこで今回は、とりあえず「収集」の観点で、サーバのログをfluentdを使ってAWSのログ収集マネジメントサービスであるKinesis Firehoseに送信し、S3に保存するところまでを作成してみた。

環境

対象サーバ:ローカルPCのVirtualBox上に作ったCentOS7.5
対象ログ:Apacheアクセスログ

f:id:xpost:20180911212149p:plain:w500

作業

S3バケットの作成

ここではすでに作成済みとする。
リージョン:東京
バケット名:awslearn-bucket

Kinesis Firehoseの作成

AWSのサービス一覧から「Kinesis」で検索し、「Kinesis Firehose 配信ストリーム」の「配信ストリームを作成する」をクリックする。

リージョン:東京
Delivery stream name:awslearn-stream
DestinationAmazon S3
S3 bucket:awslearn-bucket
Prefix:firehose_awslearn
その他:デフォルト

ログ収集対象サーバの設定

apacheのインストール・起動を行う。インストールが完了したら、ブラウザからwebサーバにアクセスできることを確認する。

# yum install -y httpd
# systemctl stop firewalld 
# systemctl disable firewalld
# systemctl start httpd

aws cliのインストールを行う。fluent-plugin-kinesisプラグインaws cliに設定したIAMユーザーのアクセスキーIDとシークレットアクセスキーを使ってKinesis Firehoseにログ転送を行う。対象サーバがEC2インスタンスであればIAMロールの割り当てでもよい。この作業は検証のため、IAMにはAdministrator権限を付与した。必要なアクセス権限については以下を参照。

Amazon Kinesis Data Firehose によるアクセスの制御 - Amazon Kinesis Data Firehose

# yum install epel-release
# yum install python-pip
# pip install pip --upgrade
# pip install awscli --user

awsコマンドのパスを通す。

# vi ~/.bash_profile

===
- PATH=$PATH:$HOME/bin
+ PATH=$PATH:$HOME/bin:$HOME/.local/bin
===

アクセスキーIDとシークレットアクセスキーを設定する。

# aws configure

td-agent(fluentd)とfluent-plugin-kinesisプラグインをインストールする。

# curl -L https://toolbelt.treasuredata.com/sh/install-redhat-td-agent2.5.sh | sh
# td-agent-gem install fluent-plugin-kinesis

td-agentの実行ユーザをtd-agentからrootに変更する。
td-agentのままだと、apacheアクセスログが読み込めず、Permission Deniedで怒られる。td-agentの実行ユーザを変えなくても、/var/log/httpdディレクトリに一般ユーザの実行権限を与えれば解消するらしい。

# vi /lib/systemd/system/td-agent.service

===
...

[Service]
- User=td-agent
+ User=root
- Group=td-agent
+ Group=root

...
===

td-agentの設定を変更する。

# cat /etc/td-agent/td-agent.conf

<source>
  @type tail
  format apache2
  path /var/log/httpd/access_log
  pos_file /var/log/td-agent/httpd-access.pos
  tag log.httpd.access
</source>

<match log.httpd.*>
  @type kinesis_firehose
  delivery_stream_name awslearn-stream
  region ap-northeast-1
</match>

td-agentを起動する。

# systemctl start td-agent

全ての設定が完了したら、webサーバにブラウザからアクセスする。5分ほど待つと、S3バケットapacheアクセスログが収集されていることが確認できる。 f:id:xpost:20180911205042p:plain

fluentdの機能でしっかりjson化もされている。

{"host":"192.168.56.1","user":null,"method":"GET","path":"/","code":403,"size":4897,"referer":null,"agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36"}
{"host":"192.168.56.1","user":null,"method":"GET","path":"/noindex/css/fonts/Bold/OpenSans-Bold.woff","code":404,"size":239,"referer":"http://192.168.56.105/noindex/css/open-sans.css","agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36"}
{"host":"192.168.56.1","user":null,"method":"GET","path":"/noindex/css/fonts/Light/OpenSans-Light.woff","code":404,"size":241,"referer":"http://192.168.56.105/noindex/css/open-sans.css","agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36"}
{"host":"192.168.56.1","user":null,"method":"GET","path":"/noindex/css/fonts/Bold/OpenSans-Bold.ttf","code":404,"size":238,"referer":"http://192.168.56.105/noindex/css/open-sans.css","agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36"}
{"host":"192.168.56.1","user":null,"method":"GET","path":"/noindex/css/fonts/Light/OpenSans-Light.ttf","code":404,"size":240,"referer":"http://192.168.56.105/noindex/css/open-sans.css","agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36"}
...

AWS EBSスナップショットを取得し、世代管理するマネージメントサービス Amazon Data Lifecycle Manager

前回、AMIを自動で取得し、世代管理するLambda関数を作った。

 

xpost.hatenablog.com

 

次はEBSスナップショットに絞った世代管理のスクリプトを書こうかなと考えていたところ、すでにAWSのマネージメントサービスとして自動でEBSスナップショットを取得し、世代管理する機能が提供されていることを知った。2018年8月には東京リージョンにも対応しているようだ。

 

dev.classmethod.jp

 

おいおい、まじかよ。自動バックアップの運用バッチなんてもうほとんどいらなくなったじゃないか…。

 

今のところ、12時間おき、24時間おきにしかEBSスナップショットをとれないようだが、そのうち1週間おきなど選択できるようになるだろう。

 

ひとつ気になったのは取得したスナップショットのデータの整合性は担保してくれていないことだ。公式ドキュメントでも整合性が壊れるみたいなことが記述されている(いまいちニュアンスが読み取れないが…)。

 

aws.amazon.com

 

AWSのマニュアルでも、EBSスナップショットのデータ整合性を担保したいなら、事前にディスクをアンマウントするか、サーバを停止しておくことを推奨しているので、今後DLMもスナップショット取得前にサーバを一時停止するようなオプションが追加されるかもしれない。

 

少なくともそういうオプションが追加されるまでは、データの整合性を担保する必要のあるサーバは、別の処理でサーバを停止しておくか、DLMを使わずに、一番上で紹介したAMIの自動取得Lambda関数でRebootオプションを有効にするなど、考える必要がありそうだ。

自己紹介

1993年生まれで、東京都で一人暮らしをしています。

現在はシステムエンジニアをしています。

インフラ構築が主な仕事なのでプログラミングは得意ではありません。

なので最近はPythonを勉強中です。

興味のある技術・製品
 
 

 

AWS EC2インスタンスを自動起動・自動停止するLambda関数

EC2インスタンス自動起動・自動停止を行う必要があったので、Lambda関数で処理を実装してみた。

社内システムにEC2を利用していると、利用料金を抑えるために夜間停止を実施している企業も多いのではないだろうか。

ランタイムはPython3.6。
CloudWatch Eventsをトリガーにし、8:00に自動起動、21:00に自動停止するようにする。

EC2インスタンスのタグに以下の設定すると、自動起動・自動停止の対象になるようにする。

処理 Key Value
自動起動 AutoStart 1
自動停止 AutoStop 1


Lambda関数に設定するIAMロールは自動起動と自動停止で共用のものを一つ作った。

{
  "Version": "2012-10-17",
  "Statement": [
      {
          "Effect": "Allow",
          "Action": [
              "logs:CreateLogStream",
              "logs:PutLogEvents"
          ],
          "Resource": "arn:aws:logs:*:*:*"
      },
      {
          "Effect": "Allow",
          "Action": [
              "ec2:DescribeInstances",
              "ec2:StartInstances",
              "ec2:StopInstances"
          ],
          "Resource": "*"
      },
      {
          "Effect": "Allow",
          "Action": "logs:CreateLogGroup",
          "Resource": "arn:aws:logs:*:*:*"
      }
  ]
}


コードは以下の二つになる。

■StartEC2Instance

import boto3
import logging
from botocore.exceptions import ClientError

ec2 = boto3.client('ec2')
logger = logging.getLogger()
logger.setLevel(logging.INFO)

def get_instances():
    instances = []
    _instances = ec2.describe_instances(
        Filters=[
            {'Name': 'tag:AutoStart', 'Values': ['1']},
            ]
        )

    for Reservations in _instances['Reservations']:
        for _instance in Reservations['Instances']:
            tags = { tag['Key']: tag['Value'] for tag in _instance['Tags'] }
            instance_name = tags.get('Name') 

            instance ={}
            instance.update({'Name': instance_name})
            instance.update({'InstanceId': _instance['InstanceId']})
            instances.append(instance)

    return instances

def start_instance(instance):

    instance_name = instance['Name']
    instance_id = instance['InstanceId']

    try:
        ec2.start_instances(
            InstanceIds = [instance_id],
            )
    except ClientError as e:
        logger.error('インスタンスの起動に失敗しました: %s(%s)', instance_name, instance_id)
        logger.exception('Received error: %s', e)
    else:
        logger.info('インスタンスの起動に成功しました: %s(%s)', instance_name, instance_id)


def lambda_handler(event, context):

    instances = get_instances()

    if not instances: return 0

    for instance in instances:
        start_instance(instance)

    return 0


■StopEC2Instance

import boto3
import logging
from botocore.exceptions import ClientError

ec2 = boto3.client('ec2')
logger = logging.getLogger()
logger.setLevel(logging.INFO)

def get_instances():
    instances = []
    _instances = ec2.describe_instances(
        Filters=[
            {'Name': 'tag:AutoStart', 'Values': ['1']},
            ]
        )

    for Reservations in _instances['Reservations']:
        for _instance in Reservations['Instances']:
            tags = { tag['Key']: tag['Value'] for tag in _instance['Tags'] }
            instance_name = tags.get('Name') 

            instance ={}
            instance.update({'Name': instance_name})
            instance.update({'InstanceId': _instance['InstanceId']})
            instances.append(instance)

    return instances

def stop_instance(instance):

    instance_name = instance['Name']
    instance_id = instance['InstanceId']

    try:
        ec2.stop_instances(
            InstanceIds = [instance_id],
            )
    except ClientError as e:
        logger.error('インスタンスの停止に失敗しました: %s(%s)', instance_name, instance_id)
        logger.exception('Received error: %s', e)
    else:
        logger.info('インスタンスの停止に成功しました: %s(%s)', instance_name, instance_id)


def lambda_handler(event, context):

    instances = get_instances()

    if not instances: return 0

    for instance in instances:
        stop_instance(instance)

    return 0


boto3のstart_instances関数・stop_instances関数はEC2のインスタンスIDのリストを渡せば、一括で起動・停止してくれるのだが、そのうち一台の処理に失敗すると、すべてのEC2インスタンスが起動・停止せずに処理が終了してしまった。結果、forループで回して一台づつ処理していく作りになった。

テストしてみたところ一台の処理に600msくらいかかるようなので、EC2インスタンスの台数に応じて制限時間を長く設定すべきだろう。

これでEC2インスタンス自動起動・自動停止ができるようになった。

ただ、本番環境サーバの運用バッチにLambdaを使うのが正しいとはあまり思えない。

ジョブスケジューラとして使う場合、処理が失敗してもLambdaはリトライ処理をかけてくれないし、AWSAPIに投げた処理は非同期で行われるので、バッチ処理内でAWSの処理が成功するのを確認するとなると、すぐに制限時間の5分を超えてしまいそうだ。

最悪運用バッチが失敗しても、「まあ、いっか」と思える環境で使うだけに留めるのが正解だと思う。

あと、Lambdaの実行結果はデフォルトでCloudWatch Logsに吐かれるようなので、処理が失敗したときの通知方法も別途考えておく必要がありそうだ。

 

殺戮の天使とかいうアニメ

金曜日に、仕事から帰ってテレビつけたらなんかやってた。

 

最近アニメは全然見ていないのだが、何となく見入ってしまった。

 

レイチェル・ガードナー…。

レイチェル・ガードナー可愛い!!

 

正直話の内容はよくわからなかったが、こういうずっと夢の中にいるようなふわふわした雰囲気のアニメは、仕事で疲れてボヤーッとした脳みそと相性がいい。

 

AmazonPrimeで公開していたので、本日最新話まで一気見した。

 

最初から最後までずっと辛気臭い雰囲気ではあったが、物語の結末が気になる魅力を感じたので今後も継続的に見ていこうと思う。

AWSアーキテクトアソシエイト受験

AWSアーキテクトアソシエイトに合格した。

得点率は85%。

 

以前取得したAzure70-533は業務命令で勉強を始めたため、やる気が起きなかった。

 

しかし、AWSは前々から興味があったので自主的に勉強することができた。

 

まあ、取得したところで良いことなんて何もないんだけどね!