【SSLVPN】OPNsense on Azure

クラウドへリモートアクセス環境を準備する場合,クラウドネイティブな方法もあるがVPN仮想アプライアンスをたててオンプレライクにSSLVPNで乗り込みたいシーンがよくある。(私の案件では)
クラウドにアカウントを作りたくない,プロトコルが対応していない等いくつか理由があるが,一番の理由は「クラウド構築担当している人がVPNとか全然分からんからNW部隊に丸投げしてくる。そんでもってユーザ管理や運用はそっちでやってね。」というもの。(

ぼやいても仕方ないので毎度VPN対応のFW仮想マシンをたてて構築するのだけれど,まぁ高いのなんの。そこで丁度よさそうなのがOPNsenseだったので構築手順を残しておく。

公式ドキュメントがあるので基本それに従って進めていく。
https://docs.opnsense.org/manual/how-tos/installazure.html

構成イメージ

バージョンは以下の通り。

サーバ側
OPNsense 24.1.1-amd64
FreeBSD 13.2-RELEASE-p9
OpenSSL 3.0.13

クライアント側
OpenVPN GUI v11.47.0.0

仮想マシン作成

ユーザ設定で認証を証明書にするとデプロイ失敗するのでパスワードを設定する。
パブリックIPとネットワークセキュリティグループを適切に設定してデプロイ完了したらHTTPSでログイン。

続いてSSLVPNの設定へ。

OPNsense設定

まずは認証関連の設定。今回は自己証明書で構成を組む。

CA作成

System: Trust: Authorities
※ 中身は省略

サーバ証明書作成

System: Trust: Certificates
※ 中身は省略

2FA用のGoogleAuth登録

System: Access: Servers
サーバ名を入れて,TypeでLocal+OTP。それ以外はそのまま。

SSLVPN Server作成

Serverを作成して~という流れはLegacyとあるのでインスタンスの作成。その前にTLS Keyを作成する。

認証用なのでAuthオンリーで。

入力したパラメータ一覧は以下の通り。それ以外は空欄ないしはデフォルトのまま。

GeneralSetting

  • Role: Server
  • Description: 適宜
  • Server(IPv4):172.16.1.0/24

Trust

  • Certificate: SSLVPN CRT(作成したサーバ証明書)
  • Certificate Authority: SSLVPN CA(作成したローカルCA)
  • Verify Client Certificate: none
  • Certificate Depth:One(Client+Server)
  • TLS static key: TSL-Key(作成したもの)
  • Auth:SHA256
  • Data Ciphers: AES-256-GCM

Authentication

  • Authentication: Google Auth(先ほど作成したもの)
  • Renegotiate time: 0 ※ OTP認証のため再接続になるとTokenが切れているため認証エラーとなってしまう。そのため,ここは「0」に設定する必要がある。OTPでなければ不要。

Routing

  • Local Network: 10.0.0.0/16 (VNetにアクセスできれば良いのでVNetに割り当てたセグメント指定)

SNAT設定

VPNのトラフィックをインタフェースでSNATする。
Modeをマニュアルへ変更。

Interface,Source addressをそれぞれ設定。

結果。

リモートアクセスユーザ作成

ユーザ名,パスワードを入力し,OTP用の設定を入力する。

ここでSaveをするとユーザ証明書作成ページに跳ぶ。自己証明書作成を選択し、その他項目も適宜入力して作成する。

その後,GoogleAuthenticatorへ登録するためのQRコードが生成されるのでスマホアプリで登録する。

OpenVPNClient設定エクスポート

TypeはFileonly。Windowsの証明書ストアのオプションは外す。

一番下にあるユーザ用の構成ファイルをダウンロードして完了。

FW設定

今回,Azure上で構築しているので通信制御はNetworkSecurityGroupで対応することとして,OPNsenseのルールは全許可とした。よってここは省略。(LANとOpenVPNのIFにIN全許可ルールを1行入れたのみ)

接続試験(Windows11)

サーバ側でも設定した再認証させない設定をダウンロードしたファイルにも追記する必要がある。

dev tun
persist-tun
persist-key
auth SHA256
client
verb 5  # デバッグ用
ping-restart 0  # これ0に指定する必要がある
resolv-retry infinite

保存した設定ファイルをOpenVPNクライアントにインポートし,接続すると認証画面が出るので「 Token + Password 」の並びで入力する。これが分からなくてサーバ設定見直したりキャプチャ眺めたり,VM作り直したりとかなり迷走した orz。

無事に成功するとIPアドレスが割り当てられAzure内に通信できるようになる。

疎通確認

無事Pingも通った。

ping 172.16.1.1  # Tunnel対向

172.16.1.1 に ping を送信しています 32 バイトのデータ:
172.16.1.1 からの応答: バイト数 =32 時間 =187ms TTL=64
172.16.1.1 からの応答: バイト数 =32 時間 =186ms TTL=64

172.16.1.1 の ping 統計:
    パケット数: 送信 = 2、受信 = 2、損失 = 0 (0% の損失)、
ラウンド トリップの概算時間 (ミリ秒):
    最小 = 186ms、最大 = 187ms、平均 = 186ms
Ctrl+C
^C

>ping 10.0.0.5 -t # VPNサーバAzure内部IP

10.0.0.5 に ping を送信しています 32 バイトのデータ:
10.0.0.5 からの応答: バイト数 =32 時間 =187ms TTL=64
10.0.0.5 からの応答: バイト数 =32 時間 =187ms TTL=64

10.0.0.5 の ping 統計:
    パケット数: 送信 = 2、受信 = 2、損失 = 0 (0% の損失)、
ラウンド トリップの概算時間 (ミリ秒):
    最小 = 187ms、最大 = 187ms、平均 = 187ms
Ctrl+C
^C

>ping 10.0.0.4 -t # Azure内同一Subnet別インスタンス

10.0.0.4 に ping を送信しています 32 バイトのデータ:
10.0.0.4 からの応答: バイト数 =32 時間 =189ms TTL=63
10.0.0.4 からの応答: バイト数 =32 時間 =189ms TTL=63

10.0.0.4 の ping 統計:
    パケット数: 送信 = 2、受信 = 2、損失 = 0 (0% の損失)、
ラウンド トリップの概算時間 (ミリ秒):
    最小 = 189ms、最大 = 189ms、平均 = 189ms
Ctrl+C
^C

以上で完了。おつかれさまでした。

VLAN Trunkポートのキャプチャ

Trunkポートを流れるパケットのVLANタグをWiresharkでキャプチャしたい。と思ったときに思いのほか手順が多かったのでメモとして残す。

ざっくり手順

上記構成でパケットキャプチャの手順は次の通り。

  1. SW1にてキャプチャの設定
  2. キャプチャを行うPCで802.1qのタグを落とさないよう,キャプチャを取得するNICのドライバ設定
  3. Wiresharkで802.1qのタグをキャプチャ画面の列に表示するよう設定

通常のアクセスポートでキャプチャを行うだけであれば意識することなくできると思うが,VLAN Trunkポートにおいては各手順でいつもと違う設定が必要になる。

SW1でmonitor session 設定

通常のキャプチャであれば

monitor session 1 source interface Gi0/1
monitor session 1 destination interface Gi0/2

とシンプルに設定をすればよいが,デフォルトではタグ情報を外して宛先ポートへパケットが転送されるため「encapsulation」オプションで「replicate / dot1q」を指定する必要がある。カプセル化でISLを設定するケースはまず無いと思うので,このオプションはdot1qでTrunk設定しているならばどちらでも良いと思う。オプションの違いは,送信元IFと同じカプセル化・設定を指定する場合は「replicate」,dot1qの設定とするならば「dot1q」という感じ。(※ IOSの場合)

なお,受信パケットでにおいて特定のVLANを限定して取得したい場合は「ingress dot1q vlan」オプションでVLAN IDを指定する。

(任意)宛先インターフェイスで IEEE 802.1Q カプセル化方式の使用を指定するには、 encapsulation dot1q を入力します。
(任意)送信元インターフェイスのカプセル化方式が宛先インターフェイスで複製されるように指定するには、 encapsulation replicate を入力します。これを選択しない場合、デフォルトでは、パケットがネイティブ形式(タグなし)で送信されます。
宛先ポートでの着信トラフィックの転送をイネーブルにして、カプセル化タイプを指定するには、 ingress をキーワードと一緒に入力します。
– dot1q vlan vlan-id: デフォルトの VLAN として指定した VLAN で、IEEE 802.1Q でカプセル化された着信パケットを受信します。
– untagged vlan vlan-id または vlan vlan-id: デフォルトの VLAN として指定した VLAN で、タグなしでカプセル化された着信パケットを受信します。

cisco.com/c/ja_jp/td/docs/sw/lanswt-access/cat2960swt/cg/005/swcfg/swspan.html

よって次のような設定を入れる。

monitor session 1 source interface Gi0/1
monitor session 1 destination interface Gi0/2 encapsulation replicate  # dot1q でもOK

NICの設定

およそのNICはデフォルトでVLANタグを落とす設定になっているため,NICの詳細設定で「Packet Priority&VLAN」を「Disable」にする。※デフォルトでは「Enable」になっているはず

この設定ができない(VLANタグをそのまま受信できない)NICもあるようで,NICの対応状況は予めチェックが必要。今回利用したNICはStarTechのUSBタイプ。

Wiresharkの設定

Wiresharkのキャプチャ画面の項目(列)にはVLAN情報がデフォルトでは表示されないため,VLAN IDを表示させるため外観設定を変更する。パケットを選択すればVLANフィールドがあるので別に要らないという人はここはやらなくてもOK。ただぱっと見どのVLANか分かるので可視性はよい。

Wiresharkを立ち上げ,「編集」→「設定」で設定画面表示。

「+」ボタンを押してフィールドを追加し,フィールドに「vlan.id」を入力する。題名はお好きなように。ここでは「VLAN」とした。

これで準備完了。

キャプチャする

VLANが表示されて幸せになれる。

ちなみに,QinQを表示させたい場合は,Wiresharkで追加するフィールドに「ieee8021ad.id1」を入力する。

  1. Display Filter Reference: IEEE 802.1ad (https://www.wireshark.org/docs/dfref/i/ieee8021ad.html ↩︎

phpIPAMインストールメモ

IPAM: IP Address Management

その名の通りアドレス管理ツールで,以前NetBoxを遣ってみたがアドレス管理だけに焦点をあてると too muchな感じだったので別のものを探していた。

そこでphpipamを見かけたので試した。

結論からいうと,こちらのほうが自分には合っていた。

OpenStackにMiracleLinux9をたて,そこへインストールする。

事前準備

必要要件を確認。正直PHPなど触ったことも無いので良くわからない。

https://phpipam.net/documents/installation/

  • Webserver (今回はnginx にした)
  • Mysql server (Mariadb)
  • PHP
  • PHP module
  • php PEAR

まず上記含めてインストールに必要なものを準備。

# dnf install https://dl.fedoraproject.org/pub/epel/epel-release-latest-9.noarch.rpm
# dnf install nginx
# dnf install mariadb-server 
# dnf install php
# dnf install php-{pdo,pdo_mysql,session,sockets,openssl,gmp,ldap,xml,json,gettext,filter,pcntl,pear,gd}
# dnf install git
# systemctl enable nginx php-fpm mariadb
# systemctl start nginx php-fpm mariadb

phpipamダウンロード。

# git clone  --recursive https://github.com/phpipam/phpipam.git /var/www/phpipam
# cd /var/www/phpipam
# git checkout -b 1.5 origin/1.5
# chown -R nginx:nginx /var/www/

Mariadb初期設定。

# mysql_secure_installation
NOTE: RUNNING ALL PARTS OF THIS SCRIPT IS RECOMMENDED FOR ALL MariaDB
      SERVERS IN PRODUCTION USE!  PLEASE READ EACH STEP CAREFULLY!

In order to log into MariaDB to secure it, we'll need the current
password for the root user. If you've just installed MariaDB, and
haven't set the root password yet, you should just press enter here.

Enter current password for root (enter for none):
OK, successfully used password, moving on...

Setting the root password or using the unix_socket ensures that nobody
can log into the MariaDB root user without the proper authorisation.

You already have your root account protected, so you can safely answer 'n'.

Switch to unix_socket authentication [Y/n] y
Enabled successfully!
Reloading privilege tables..
 ... Success!


You already have your root account protected, so you can safely answer 'n'.

Change the root password? [Y/n] y
New password:
Re-enter new password:
Password updated successfully!
Reloading privilege tables..
 ... Success!


By default, a MariaDB installation has an anonymous user, allowing anyone
to log into MariaDB without having to have a user account created for
them.  This is intended only for testing, and to make the installation
go a bit smoother.  You should remove them before moving into a
production environment.

Remove anonymous users? [Y/n] y
 ... Success!

Normally, root should only be allowed to connect from 'localhost'.  This
ensures that someone cannot guess at the root password from the network.

Disallow root login remotely? [Y/n] y
 ... Success!

By default, MariaDB comes with a database named 'test' that anyone can
access.  This is also intended only for testing, and should be removed
before moving into a production environment.

Remove test database and access to it? [Y/n] y
 - Dropping test database...
 ... Success!
 - Removing privileges on test database...
 ... Success!

Reloading the privilege tables will ensure that all changes made so far
will take effect immediately.

Reload privilege tables now? [Y/n] y
 ... Success!

Cleaning up...

All done!  If you've completed all of the above steps, your MariaDB
installation should now be secure.

Thanks for using MariaDB!

ipam用DBユーザ作成。

# mysql -u root -p
Enter password:
Welcome to the MariaDB monitor.  Commands end with ; or \g.
Your MariaDB connection id is 13
Server version: 10.5.16-MariaDB MariaDB Server

Copyright (c) 2000, 2018, Oracle, MariaDB Corporation Ab and others.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

MariaDB [(none)]> create database phpipam;
Query OK, 1 row affected (0.000 sec)

MariaDB [(none)]> GRANT ALL on phpipam.* to phpipam@localhost identified by 'PASSWORD';
Query OK, 0 rows affected (0.050 sec)

MariaDB [(none)]> exit
Bye

スキーマ取り込み。

# mysql -u root -p phpipam < ./db/SCHEMA.sql

設定ファイル編集

設定ファイルをコピー。

# cp /var/www/phpipam/config.dist.php /var/www/phpipam/config.php

DB認証関連パラメータを編集。

# vi /var/www/phpipam/config.php
/**
 * database connection details
 ******************************/
$db['host'] = '127.0.0.1';
$db['user'] = 'phpipam';
$db['pass'] = 'phpipamadmin';
$db['name'] = 'phpipam';
$db['port'] = 3306;


/***
 このままいくと以下メッセージが出るのでPHPのバージョンをサポート外でも起動させるオプションを入れる 
Detected PHP version: 8.0.27 >= 8.0
***/

$allow_untested_php_versions=true;

php.iniのタイムゾーンを設定。

# vim /etc/php.ini
[Date]
; Defines the default timezone used by the date functions
; http://php.net/date.timezone
date.timezone = Asia/Tokyo

nginx周りの設定ファイル編集。

/etc/nginx/nginx.conf のサーバセクションをコメントアウト。

# vi /etc/nginx/nginx.conf
#    server {
#        listen       80;
#        listen       [::]:80;
#        server_name  _;
#        root         /usr/share/nginx/html;
#
#        # Load configuration files for the default server block.
#        include /etc/nginx/default.d/*.conf;
#
#        error_page 404 /404.html;
#        location = /404.html {
#        }
#
#        error_page 500 502 503 504 /50x.html;
#        location = /50x.html {
#        }
#    }

phpipam用に以下設定ファイル作成。公式参照しつつググった結果をもとに適宜修正。

# vi /etc/nginx/conf.d/phpipam.conf
server {
    # root directory
    root   /var/www/phpipam/;
   
    # phpipam
    location / {
        try_files $uri $uri/ index.php;
        index index.php;
    }
    # phpipam - api
    location /api/ {
        try_files $uri $uri/ /api/index.php;
    }

    # php-fpm
    location ~ \.php$ {
        fastcgi_pass   unix:/var/run/php-fpm/www.sock;
        fastcgi_index  index.php;
        try_files      $uri $uri/ index.php = 404;
        fastcgi_param  SCRIPT_FILENAME $document_root$fastcgi_script_name;
        include        fastcgi_params;
    }
 }

php-fpm.conf のソケットのパスを適切に修正。

# vi /etc/nginx/conf.d/php-fpm.conf
# PHP-FPM FastCGI server
# network or unix domain socket configuration

upstream php-fpm {
        server unix:/var/run/php-fpm/www.sock;
}

アクセス許可

SELinuxとfirewalldでHTTP(80)を許可する。

# firewall-cmd --add-service=http --zone=public --permanent
# firewall-cmd --reload
# sudo setsebool -P httpd_can_network_connect 1

初期設定

これでhttp://ipaddress/ にアクセスすると以下画面が出て初期セットアップに入る。

ポチポチやっていくと初期パスでログイン→パス変更→ダッシュボードへと遷移する。

これでセットアップは完了。後はIPアドレス管理のための準備となるが割愛。

FSのワイヤレス設定

先日FSの無線LANコントローラとAPが届いたのでセットアップを行った。かなりCiscoと勝手が違うため結構困惑したので最低限のメモを残す。(設定の手がかりが公式のドキュメント(英語PDF)のみで,環境構築までの一連の流れが正直わかりにくかった。)

※ 2023/3/11 コントローラ設定でdhcp-detectを無効化しないとコントローラーIPが再起動時に消えてしまう問題があったため追記。

調達したのは以下の通り。

  • 無線LANコントローラ AC-1004
  • 無線LANアクセスポイント AP-N505

構成は端折ってこんな感じ。VLAN30(192.168.30.0/24)を管理セグメントとしているので,コントローラ(以下AC)もAPもVLAN30のIPアドレスを割り当てる。AC~AP間のメッセージはAPの管理IPはデフォルトVLANを使うため,APが接続するポートはVLAN30をデフォルトVLANに設定する。

AC(コントローラ)設定

コンソールポートに接続し(初期admin/admin),ホスト名,管理IPアドレス,CAPWAP用IPアドレス,AP接続用ポート設定を行う。CiscoライクなCLIなのでコマンドで困ることはほぼなかった。

conf t
!
hostname home-wlc
!
! ※ ↓これをいれないとctrl-ipが再起動時に有効にならない
dhcp-detect disable
!
!
ac-controller
 capwap ctrl-ip 192.168.30.15
 country JP
!
! ※ 2023/3/11 追記この定義を入れないと設定上 capwap ctrl-ip の設定が入っていても消えてしまう
dhcp-detect disable  
!
username XXXX privilege 15 password XXXX
!
clock timezone UTC +9 0
!
interface GigabitEthernet 0/0
 description UPLINK
 switchport mode trunk
!
interface GigabitEthernet 0/1
 description to AP1
 switchport mode trunk
 switchport trunk native vlan 30
 poe enable
!
interface VLAN 30
 ip address 192.168.30.15 255.255.255.0
!
! 設定保存
end
wr 

その他DHCPなどの設定はうちでは不要なので消した。(この辺は適宜対応)

AP設定

こちらも同様にコンソールで接続して設定。最低限設定が必要なのはACのIPアドレスと自身のIPアドレスのみ。


conf t
!
acip ipv4 192.168.30.15 
apip ipv4 192.168.30.17 255.255.255.0 192.168.30.1
!
interface BVI 1
 ip address 192.168.30.17 255.255.255.0
!
! 設定保存
end
wr 

接続

これで接続すればACにAPが登録される。WebUIから確認ができ,以降はこちらで操作する。

ひとまず初期設定はこれで完了。無線設定はどうにかなるから省略。

NW機器のTACACS+設定

およそ世間では情報が沢山あるので我が家の設定値のみ記載。

設定方針は次の通り。

  • 1号機は.20,2号機は.21,TACACSサーバがダウンしたらLocal認証(タイムアウトは5秒)
  • ログインできたら即特権(コマンド権限はTACACSサーバ側で握る)
  • 1号機を最初に記述,2号機を次に記述
  • aaaの設定でdefaultとして定義するものをよく見かけるが,自宅環境では不採用とし,各aaaの定義に名前を付ける。(仮にdefaultで定義すると全てのインタフェースに適用されるし,設定順番ミスるとそのままコマンドがはじかれるなどリスクがあると思う。ただし,line vty でのauthentication設定などは不要になるなど設定行は減る。)

Cisco IOS

!!! AAA有効
aaa new-model

!!! TACACSサーバ登録。aaa tacacs-server は非推奨になった模様。
tacacs server tacacs-sv1
 address ipv4 192.168.30.20
 key <tacacsサーバで設定したKey>
 timeout 5
tacacs server tacacs-sv2
 address ipv4 192.168.30.21
 key <tacacsサーバで設定したKey>
 timeout 5

!!! TACACSサーバグループ作成。
aaa group server tacacs+ TAC_SRV
 server name tacacs-sv1
 server name tacacs-sv2

!!! TACplusという名前でTAC_SRVで登録したTACACSサーバに問い合わせる。落ちてたらローカル認証。
aaa authentication login TACplus_authe group TAC_SRV local-case


!!! 実行権限をTACACSサーバに問い合わせる。落ちてたらローカル認証。
aaa authorization exec TACplus_autho_exec group TAC_SRV local

!!! 実行レベル15のコマンド実行権限をTACACSサーバに問い合わせる。落ちてたらローカル認証。
aaa authorization commands 15 TACplus_autho_cmd group TAC_SRV local

!!! コマンド実行ログをTACACSサーバへ送信。
aaa accounting commands 15 TACplus_acco start-stop group TAC_SRV



line vty 0 4
 exec-timeout 0 0
 authorization commands 15 TACplus_autho_cmd
 authorization exec TACplus_autho_exec
 logging synchronous
 login authentication TACplus_authe
 transport input ssh

Cisco ASA

ASAの場合はIOSとは若干違うが,基本的な定義の流れは同じ。

aaa-server TACplus protocol tacacs+
 reactivation-mode timed
 max-failed-attempts 2
aaa-server TACplus (mgmtside) host 192.168.30.20
 key *****
aaa-server TACplus (mgmtside) host 192.168.30.21
 key *****

!!! SSH接続時にTACACS→LOCALという順で認証
aaa authentication ssh console TACplus LOCAL

!!! 特権
aaa authentication enable console TACplus LOCAL

aaa authorization command TACplus LOCAL
aaa accounting command privilege 15 TACplus

!!! 
aaa authorization exec authentication-server auto-enable
aaa authentication login-history

以上。

TACACS+サーバ構築

概要

宅内環境の認証をTACACS化しようと,まずはTACACS+サーバを2台立てる。構成は次の通り。各機器の管理IFと同じセグメントにTACACSサーバを2台建てる。

OpenStack上にDebianイメージでインスタンスを1つ。冗長化のためESXi上にUbuntuのVMをたてて2号機とする。(なぜOSが違うかというと,ESXi上にDebian10のイメージがなかっただけで特に他意はなし)

TACACS+はtac_plusを利用する。元はこれ(https://www.shrubbery.net/tac_plus/)だと思うのだが,公開が終わっているっぽい。facebookがGithubでforkしたものを公開していたので,Ubuntu側ではここからソースをもってきてコンパイルしてインストールした。’Debianはリポジトリからインストールできた。)

インストール

Debian 10でのインストール方法

aptで一発。

# apt install tacacs+

Debain11でのインストール方法

11にはまだパッケージがなかったためソースからコンパイル。

# apt install git build-essential flex bison libwrap0-dev
$ git clone https://github.com/facebook/tac_plus.git
$ cd tac_plus/tacacs-F4.0.4.28
$ ./configure
$ make
# make install

続いて必要なファイルの準備。

サービス起動ファイル /etc/init.d/tacacs_plus

#!/bin/sh
### BEGIN INIT INFO
# Provides:          tacacs+
# Required-Start:    $network $local_fs $syslog $remote_fs
# Required-Stop:     $network $local_fs $remote_fs
# Should-Start:      $named
# Default-Start:     2 3 4 5
# Default-Stop:      0 1 6
# Short-Description: TACACS+ authentication daemon
### END INIT INFO

PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin

DAEMON=/usr/local/sbin/tac_plus
NAME="tacacs+"
DESC="TACACS+ authentication daemon"
LOGDIR=/var/log/
STARTTIME=1

PIDFILE=/var/run/tac_plus.pid

test -x $DAEMON || exit 0

. /lib/lsb/init-functions

# Default options, these can be overriden by the information
# at /etc/default/$NAME
DAEMON_OPTS="-C /etc/tacacs+/tac_plus.conf"          # Additional options given to the server


LOGFILE=$LOGDIR/tac_plus.log  # Server logfile

# Include defaults if available
if [ -f /etc/default/$NAME ] ; then
        . /etc/default/$NAME
fi

# Check that the user exists (if we set a user)
# Does the user exist?
if [ -n "$DAEMONUSER" ] ; then
    if getent passwd | grep -q "^$DAEMONUSER:"; then
        # Obtain the uid and gid
        DAEMONUID=`getent passwd |grep "^$DAEMONUSER:" | awk -F : '{print $3}'`
        DAEMONGID=`getent passwd |grep "^$DAEMONUSER:" | awk -F : '{print $4}'`
    else
        log_failure_msg "The user $DAEMONUSER, required to run $NAME does not exist."
        exit 1
    fi
fi


set -e

running_pid() {
# Check if a given process pid's cmdline matches a given name
    pid=$1
    name=$2
    [ -z "$pid" ] && return 1
    [ ! -d /proc/$pid ] &&  return 1
    cmd=`cat /proc/$pid/cmdline | tr "\000" "\n"|head -n 1 |cut -d : -f 1`
    # Is this the expected server
    [ "$cmd" != "$name" ] &&  return 1
    return 0
}

running() {
# Check if the process is running looking at /proc
# (works for all users)

    # No pidfile, probably no daemon present
    [ ! -f "$PIDFILE" ] && return 1
    pid=`cat $PIDFILE`
    running_pid $pid $DAEMON || return 1
    return 0
}

start_server() {
# Start the process using the wrapper
    if check_config_quiet ; then
         start-stop-daemon --start --quiet --pidfile $PIDFILE \
                --exec $DAEMON -- $DAEMON_OPTS
         errcode=$?
         return $errcode
    else
         return $?
    fi

}

stop_server() {
    killproc -p $PIDFILE $DAEMON
    return $?
}

reload_server() {
    if check_config_quiet ; then
         [ ! -f "$PIDFILE" ] && return 1
         pid=`cat $PIDFILE` # This is the daemon's pid
         # Send a SIGHUP
         kill -1 $pid
         return $?
    else
         return $?
    fi
}

check_config() {
        $DAEMON -P $DAEMON_OPTS
        return $?
}

check_config_quiet() {
        $DAEMON -P $DAEMON_OPTS >/dev/null 2>&1
        return $?
}

force_stop() {
# Force the process to die killing it manually
        [ ! -e "$PIDFILE" ] && return
        if running ; then
                kill -15 $pid
        # Is it really dead?
                sleep "$DIETIME"s
                if running ; then
                        kill -9 $pid
                        sleep "$DIETIME"s
                        if running ; then
                                echo "Cannot kill $NAME (pid=$pid)!"
                                exit 1
                        fi
                fi
        fi
        rm -f $PIDFILE
}


case "$1" in
  start)
        log_daemon_msg "Starting $DESC " "$NAME"
        # Check if it's running first
        if running ;  then
            log_progress_msg "apparently already running"
            log_end_msg 0
            exit 0
        fi
        if start_server ; then
            # NOTE: Some servers might die some time after they start,
            # this code will detect this issue if STARTTIME is set
            # to a reasonable value
            [ -n "$STARTTIME" ] && sleep $STARTTIME # Wait some time
            if  running ;  then
                # It's ok, the server started and is running
                log_end_msg 0
            else
                # It is not running after we did start
                log_end_msg 1
            fi
        else
            # Either we could not start it
            log_end_msg 1
        fi
        ;;
  stop)
        log_daemon_msg "Stopping $DESC" "$NAME"
        if running ; then
            # Only stop the server if we see it running
                        errcode=0
            stop_server || errcode=$?
            log_end_msg $errcode
        else
            # If it's not running don't do anything
            log_progress_msg "apparently not running"
            log_end_msg 0
            exit 0
        fi
        ;;
  force-stop)
        # First try to stop gracefully the program
        $0 stop
        if running; then
            # If it's still running try to kill it more forcefully
            log_daemon_msg "Stopping (force) $DESC" "$NAME"
                        errcode=0
            force_stop || errcode=$?
            log_end_msg $errcode
        fi
        ;;
  restart|force-reload)
        log_daemon_msg "Restarting $DESC" "$NAME"
                errcode=0
        stop_server || errcode=$?
        # Wait some sensible amount, some server need this
        [ -n "$DIETIME" ] && sleep $DIETIME
        start_server || errcode=$?
        [ -n "$STARTTIME" ] && sleep $STARTTIME
        running || errcode=$?
        log_end_msg $errcode
        ;;
  status)

        log_daemon_msg "Checking status of $DESC" "$NAME"
        if running ;  then
            log_progress_msg "running"
            log_end_msg 0
        else
            log_progress_msg "apparently not running"
            log_end_msg 1
            exit 1
        fi
        ;;
  # Use this if the daemon cannot reload
  reload)
        log_daemon_msg "Reloading $DESC configuration files" "$NAME"
        if reload_server ; then
                if running ; then
                        log_end_msg 0
                else
                        log_progress_msg "$NAME not running"
                        log_end_msg 1
                fi
        else
                log_progress_msg "Reload failled"
                log_end_msg 1
        fi
        ;;
  check)
        check_config
        if [ X$? = "X0" ]
        then
                log_daemon_msg "Checking $DESC configuration files successful" "$NAME"
        else
                log_daemon_msg "Checking $DESC configuration files failed"
                exit 1
        fi
        ;;
  *)
        N=/etc/init.d/tacacs_plus
        echo "Usage: $N {start|stop|force-stop|restart|reload|force-reload|status|check}" >&2
        exit 1
        ;;
esac

exit 0

アカウンティングログファイル作成

# touch /var/log/tac_plus.acct

tacacs+設定ファイル編集(2台共通)

/etc/tacacs+/tac_plus

以下設定例。今回はとりあえず設定ファイル中にユーザを記述する設定方式をとったが,仕事ではやらないように注意。パスワードのハッシュ化は 「openssl passwd – 5」 (SHA256)で生成。

# アカウンティング用ログファイル指定
accounting file = /var/log/tac_plus.acct


# <サーバと通信するための鍵を記述>
key = tacacs_key

user = networkmanager {
        name = "Network Manager"
        member = admin
        login = des <ハッシュ化パスワード>
        enable = des <ハッシュ化enable>
}

user = networkoperator {
        name = "Read only user"
        member = operator
        login = des <ハッシュ化パスワード>
        enable = des <ハッシュ化enable>
}

# 管理者用グループ。Privilage15。
group = admin {
        default service = permit
        service = exec {
                priv-lvl = 15
        }

# オペレータ用グループ。showコマンドのみ利用可能
group = operator {
        default service = deny
        service = exec {
                priv-lvl = 15
        }
        cmd = show {
                permit .*
        }
        cmd = exit {
                permit .*
        }
}

TACACS+起動

# service tacacs_plus start

以上。

次はNW機器側の設定。

Catalyst3560CでGREトンネル

とある検証でL3SWでGREトンネルのみ張る設定を確認した。普通にいけるかなと思ったが,keepaliveでひっかかったのでメモ。

基本的には,Vlan IFにIPアドレスを設定し,tunnel source / destination でそれぞれ指定すれば完了。ただし,Keepaliveを設定するとDownとなる。デバッグをとっても応答が返ってこず(recieveが無い)Down判定されていた。

interface Tunnel1
 ip address 192.168.254.2 255.255.255.252
 keepalive 5 2
 tunnel source Vlan1
 tunnel destination 192.168.10.1

なんでだろうなと調べてみると,以下の一文を見つけた。

Support is available for gre ip tunnel mode. The tunnel source can be loopback and Layer 3 (physical or Etherchannel) interfaces only.

https://www.cisco.com/c/dam/en/us/td/docs/switches/lan/catalyst3850/software/release/16-1/workflows/gre-feature-guide.pdf

なるほど。型番もバージョンも違うけれどこれっぽい。試しにip routingを有効にしてLoopback IFを追加,対向のLoに対するスタティックルート追加したところ無事にkeepalive有りでトンネルが張れた。

枯れた技術だけれど,結構まだハマるところがあるという。精進が足りませんね。

LinuxでPBR

AWSでインスタンスを立ち上げたとき,2つのサブネットに所属させ一方は社内ネットワーク,一方はメンテナンス用に外部(インターネット)からSSHアクセス用にと構成することがある。

この場合,それぞれのNICに着信したトラフィックはそのNICから応答を返して欲しいが通信元が不特定だとスタティックルートで処理するのは不可能。このような時はPBRを設定して対応する。
参考) 

手順

  • ルートテーブルの作成
  • ポリシーの作成
  • ルーティングの追加
  • 永続的設定

ルートテーブルの作成

/etc/iproute2/rt_tables にens192用のルーティングテーブル「rt10」とens224用のルーティングテーブル「rt100」を追加する。なお,IDは1~255まであり,254はmainテーブル,255はlocalでリザーブされている。ここでは100と101を使用する。
#
# reserved values
#
255 local
254 main
253 default
0 unspec
#
# local
#
#1 inr.ruhep
100 rt100 #追加
101 rt10 #追加

ポリシーの作成

書式: ip rule add from <IF address> table <table name> priority <priority number>
# ip rule add from 192.168.10.40 table rt10 priority 100
# ip rule add from 192.168.1.40 table rt100 priority 101

prioritを指定しないとカーネルが一番古いpriorityの前に自動で採番する。何も設定されていなければmainの32766の前に順に追加される。(32765,32764という感じで)

ルーティングの追加

書式: ip route add default via <Gateway address> dev <IF name> table <table name> 
# ip route add default via 192.168.10.1 dev ens192 table rt10
# ip route add default via 192.168.1.1 dev ens224 table rt100
まずはこれでPBRは設定完了。
192.168.1.0/24にいるPCからpingのテスト。
PBR設定前はens192からreplyが返っていない。(なお,リバースパスフィルタリングが有効になっているのでens224では戻りのパケットがかえらない)
PBR設定後はしっかり着信IFからreplyが返っている。

永続的設定

コマンドで設定したルールとルートは再起動で消えるため,/etc/sysconfig/network-script配下に定義ファイルを作成するとあったが,NetworkMangerを利用している環境では利用できない。

・ ルール定義

/etc/sysconfig/network-script/rule-<IF名>
各IFに先のコマンドを記述。
ip rule add from 192.168.10.40 table rt10 priority 100
ip rule add from 192.168.1.40 table rt100 priority 101

・ ルート定義

/etc/sysconfig/network-script/route-<IF名>
各IFに先のコマンドを記述。
ip route add default via 192.168.10.1 dev ens192 table rt10
ip route add default via 192.168.1.1 dev ens224 table rt100
これがNGとなると,起動スクリプトに上のコマンドを実行するように仕込む方法が考えられる。
pbr.sh
#!/bin/sh
## create rules for pbr

ip rule add from 192.168.10.40 table rt10 priority 100
ip rule add from 192.168.1.40 table rt100 priority 101

## create default route for each routing tables
ip route add default via 192.168.10.1 dev ens192 table rt10
ip route add default via 192.168.1.1 dev ens224 table rt100
/etc/systemd/system/pbr.service
[Unit]
Description = PBR setting script
After = network-online.target # NICがオンラインにならないとコマンドが失敗に終わるため

[Service]
ExecStart = /root/pbr.sh
Type = simple

[Install]
WantedBy = multi-user.target

ここまで準備できたら
# systemctl enable pbr.service
# systemctl start pbr.service
で完了。
ただここまでやっておきながら,nmcliを使えばコマンド1行で終わることが分かった。

nmcliを使う方法

ens192の設定
nmcli con add type ethernet con-name ens192 ifname ens192 ipv4.method manual ipv4.addresses 192.168.10.40/24 ipv4.routes "0.0.0.0/1 192.168.10.1 table=100, 128.0.0.0/1 192.168.10.1 table=100" ipv4.routing-rules "priority 100 from 192.168.10.40 table 100"
ens224の設定
nmcli con add type ethernet con-name ens224 ifname ens224 ipv4.method manual ipv4.addresses 192.168.1.40/24 ipv4.routes "0.0.0.0/1 192.168.1.1 table=101, 128.0.0.0/1 192.168.1.1 table=101" ipv4.routing-rules "priority 101 from 192.168.1.40 table 101"
以上。超簡単。再起動後もこれで対応している。
RHELの公式ドキュメントによると,nmcliは0.0.0.0/0の表記に対応していないとのことで,0.0.0.0/1と128.0.0.0/1の2つ定義を入れて0.0.0.0/0をカバーしなくてはならないとのこと。この点とテーブルを名前指定ができず番号で記述する必要があるのがややわかりにくい。
ちなみにIFの設定ファイルは以下にある。
/etc/NetworkManager/system-connections/ens192.nmconnection 
/etc/NetworkManager/system-connections/ens224.nmconnection 

MikroTik CRS309を手に入れた

欲しかった10Gのスイッチを米アマ経由で調達した。
環境準備や設定・スループット測定などいろいろと手間取ったのでメモ。
購入したのMikroTik CRS309。米アマで購入当時およそ$270。輸送費やもろもろ税がかかって日本円で3.3万円程度で本体を手に入れた。
こちらは日本アマゾン

ざっと所感

– それなりにお金がかかる

・10GのSFPが必要なので,本体以外にもケーブル/デバイス側の10G NICといった費用も考えて調達しないとキツイ。なお,SFP+は(こちらも米アマで購入)SFP+とDACで大体4万円くらいかかった。
1なので,トータルで7.3万円で10GのL3SWを手に入れたことになる。

– 仕様はよくチェックしよう

公式のガイドラインで「10Gbase-TのときはRJ45モジュールが熱くなるから1ポート間隔あけて搭載することが推奨」あった。想定外だった。確かに触っていると火傷するほどアッツイ。よく調べよう。(以下,ガイドライン抜粋)
Even when using devices that come with separated SFP+ cages, for example CRS309-1G-8S+, it is still not recommended to deploy the S+RJ10 transceivers beside each other. Use S+RJ10 in every second interface to avoid transceivers overheating which may cause unpredictable behavior.

– CLIが独特

show run のような現状のConfigを確認するコマンドが無いので,CLIとWebUIを組み合わせて操作することになると思う。

– 設定も独特

bridgeやらswitchやらinterfaceやらと設定のセクション分けが困惑する。丸1日2日触っているとだいぶ慣れてくるけど,IOSやJunosのような感覚でいると訳分からなくなる。

スループット測定

– snake test

snake test
snake test イメージ
10Gに対応しているPCを2台準備できなかったので,CentOS8にDual portの10Gを積んで,network namespaceで分離しiperf3で測定した。

– 測定準備

予めNIC側のMTUを9000に設定する。
nmcli connection show でデバイスのUUIDを確認。
9000へ変更。
nmcli con mod UUID1 802-3.mtu 9000
nmcli con mod UUID2 802-3.mtu 9000
systemctl restart NetworkManager
Namespace作成
sudo ip netns add NIC1
sudo ip netns add NIC2
そえぞれのNICをNamespaceへ割り当てる。
sudo ip link set enp1s0f0 netns NIC1 up
sudo ip link set enp1s0f1 netns NIC2 up
IP設定
sudo ip netns exec NIC1 bash
ip addr add 10.10.10.1/30 dev enp1s0f0
sudo ip netns exec NIC2 bash
ip addr add 10.10.10.2/30 dev enp1s0f1
CSR309のL2MTUはMAXで10218。
interface set l2mtu=10218 1

テスト結果

およそ10Gbpsでました。
実環境ではここまでではないだろうけれど,パーソナルユースなので細かいところまではテストしない。
なお,公式でスループットのテスト結果が公開されているのでこれで十分だと思う。
この後はCisco891Mとのリプレースをやる。

Cisco IOSで設定のロールバック

先日のメモではIOSでのアーカイブについて触れたけど,今回は安全にロールバックを行う方法ついて確認。

そもそもCisco IOSでの設定即時反映は便利である反面,ミスったときの影響が大きいという問題があると感じていた。その点,JunosやVyOSのようなその他のNW OSで実装されている,設定反映前の比較やロールバックタイマーは運用を担っている人達からすると非常に助かる機能だと思う。
IOSはどうしてこの機能を盛り込まないのだろうと思っていたけれど,結構前からそれに類するコマンドはあったようで,私は知りませんでした。(12.2位からあったようで・・・)
<configure replace コマンド>
設定をロールバックするときはこのコマンドを使う。
こちらによると,稼働中のConfigとの差分のみを適用してくれるとのことで,copy xxx running-conifg のようにまるっと上書きよりも安全に変更できるそうだ。(コマンドリファレンスには置き換えるとしか書いていないけど,そうなのか)

試しにホスト名を変えてロールバックをしてみる。

R1#conf t
Enter configuration commands, one per line. End with CNTL/Z.
R1(config)#hostname Router1
Router1(config)#end
Router1#show archive
The maximum archive configurations allowed is 10.
The next archive file will be named ftp://10.10.10.10/Router1--3605
Archive # Name
1 ftp://10.10.10.10/R1-May--8-01-53-04.568-3595
2 ftp://10.10.10.10/R1-May--8-01-56-04.701-3596
3 ftp://10.10.10.10/R1-May--8-01-59-04.848-3597
4 ftp://10.10.10.10/R1-May--8-02-02-04.993-3598
5 ftp://10.10.10.10/R1-May--8-02-05-05.127-3599
6 ftp://10.10.10.10/R1-May--8-02-08-05.277-3600
7 ftp://10.10.10.10/R1-May--8-02-11-05.411-3601
8 ftp://10.10.10.10/R1-May--8-02-14-05.568-3602
9 ftp://10.10.10.10/R1-May--8-02-17-06.489-3603
10 ftp://10.10.10.10/R1-May--8-02-20-07.340-3604 <- Most Recent

ホスト名がR1からRouter1へ変わったので一つ前のArchive9へロールバックする。

Router1#configure replace ftp://10.10.10.10/R1-May--8-02-17-06.489-3603
This will apply all necessary additions and deletions
to replace the current running configuration with the
contents of the specified configuration file, which is
assumed to be a complete configuration, not a partial
configuration. Enter Y if you are sure you want to proceed. ? [no]: yes # 確認で「yes」と入力
Loading R1-May--8-02-17-06.489-3603 !
[OK - 1105/4096 bytes]

Loading R1-May--8-02-17-06.489-3603 !
[OK - 1105/4096 bytes]

Total number of passes: 1
Rollback Done

R1#

R1へ戻った。戻したい状態が明確なので,hostname R1 と設定を変更するよりも安全に戻せる。
ただし,これは即時反映のため,一定時間で元に戻したい場合は,さらにオプションで「 trigger timer <1-120(min)> 」をつけて何分後に元に戻すかを指定する。
これで,「うわぁ!ロールバック元間違えた!」という時にも安心していられる(?)。

trigger timer オプションをつけて実行し,問題なければ,「configure confirm」で確定してロールバック完了となる。

以下は,ロールバック実行/1分後にロールバック自体を元に戻す流れ。

Switch1#configure replace ftp://10.10.10.10/R1-May--8-02-47-14.947-3616 revert trigger timer 1    # 1分後にロールバックを元にもどす
Writing Switch1-May--8-02-47-53.701-3618 Rollback Confirmed Change: Backing up current running config to ftp://10.10.10.10/Switch1-May--8-02-47-53.701-3618

This will apply all necessary additions and deletions
to replace the current running configuration with the
contents of the specified configuration file, which is
assumed to be a complete configuration, not a partial
configuration. Enter Y if you are sure you want to proceed. ? [no]: yes
Loading R1-May--8-02-47-14.947-3616 !
[OK - 1105/4096 bytes]

*May 8 02:47:53.814: %ARCHIVE_DIFF-5-ROLLBK_CNFMD_CHG_BACKUP: Backing up current running config to ftp://10.10.10.10/Switch1-May--8-02-47-53.701-3618
Loading R1-May--8-02-47-14.947-3616 !
[OK - 1105/4096 bytes]

Total number of passes: 1
Rollback Done

R1#Rollback Confirmed Change: Rollback will begin in one minute. # ホスト名がR1に戻った。ロールバック完了。
Enter "configure confirm" if you wish to keep what you've configured
*May 8 02:47:55.655: Rollback:Acquired Configuration lock.
R1#
*May 8 02:47:55.771: %ARCHIVE_DIFF-5-ROLLBK_CNFMD_CHG_START_ABSTIMER: User: console(Priv: 15, View: 0): Scheduled to rollback to config ftp://10.10.10.10/Switch1-May--8-02-47-53.701-3618 in 1 minutes
*May 8 02:47:55.775: %ARCHIVE_DIFF-5-ROLLBK_CNFMD_CHG_WARNING_ABSTIMER: System will rollback to config ftp://10.10.10.10/Switch1-May--8-02-47-53.701-3618 in one minute. Enter "configure confirm" if you wish to keep what you've configured
R1#Rollback Confirmed Change: rolling to:ftp://10.10.10.10/Switch1-May--8-02-47-53.701-3618

Loading Switch1-May--8-02-47-53.701-3618 !
[OK - 1110/4096 bytes]

Loading Switch1-May--8-02-47-53.701-3618 !
[OK - 1110/4096 bytes]

!Pass 1
!List of Rollback Commands:
no hostname R1
hostname Switch1
end


Total number of passes: 1
Rollback Done

*May 8 02:48:55.776: %ARCHIVE_DIFF-5-ROLLBK_CNFMD_CHG_ROLLBACK_START: Start rolling to: ftp://10.10.10.10/Switch1-May--8-02-47-53.701-3618
*May 8 02:48:55.852: Rollback:Acquired Configuration lock.
R1#
Switch1# # 1分経過したためロールバックがもどされ,ホスト名が Switch1 に戻った。

ロールバックを確定する流れは以下のようになる。

Switch1#configure replace ftp://10.10.10.10/R1-May--8-02-47-14.947-3616 revert trigger timer 1
Writing Switch1-May--8-02-55-47.783-3621 Rollback Confirmed Change: Backing up current running config to ftp://10.10.10.10/Switch1-May--8-02-55-47.783-3621

This will apply all necessary additions and deletions
to replace the current running configuration with the
contents of the specified configuration file, which is
assumed to be a complete configuration, not a partial
configuration. Enter Y if you are sure you want to proceed. ? [no]: yes
Loading R1-May--8-02-47-14.947-3616 !
[OK - 1105/4096 bytes]

*May 8 02:55:47.855: %ARCHIVE_DIFF-5-ROLLBK_CNFMD_CHG_BACKUP: Backing up current running config to ftp://10.10.10.10/Switch1-May--8-02-55-47.783-3621
Loading R1-May--8-02-47-14.947-3616 !
[OK - 1105/4096 bytes]

Total number of passes: 1
Rollback Done

R1#Rollback Confirmed Change: Rollback will begin in one minute.
Enter "configure confirm" if you wish to keep what you've configured

*May 8 02:55:49.323: Rollback:Acquired Configuration lock.
R1#
R1#
*May 8 02:55:49.427: %ARCHIVE_DIFF-5-ROLLBK_CNFMD_CHG_START_ABSTIMER: User: console(Priv: 15, View: 0): Scheduled to rollback to config ftp://10.10.10.10/Switch1-May--8-02-55-47.783-3621 in 1 minutes
*May 8 02:55:49.431: %ARCHIVE_DIFF-5-ROLLBK_CNFMD_CHG_WARNING_ABSTIMER: System will rollback to config ftp://10.10.10.10/Switch1-May--8-02-55-47.783-3621 in one minute. Enter "configure confirm" if you wish to keep what you've configured
R1#
R1#configure confirm
R1#
*May 8 02:55:55.567: %ARCHIVE_DIFF-5-ROLLBK_CNFMD_CHG_CONFIRM: User: console: Confirm the configuration change
R1#

今後はこれを活用していきたい。

もう1点良いと思うところは,ロールバック手順で設定変更を行えば,Configの投入順序を気にしなくても良いという点。
例えばIPSecの設定で,transform-set を先に設定してからcrypto mapの中で定義しなくてはならないが,そういったところを意識しなくても良い。
流し込みミスと併せて,事前に流し込みの順序を考える必要がないというのは結構利点だと思う。
R1(config-crypto-map)#set transform-set TS
%ERROR: transform set with tag "TS" does not exist.

こういうことが防げる。