ドメイン参加(Linux)

Linux クライアントを作成したドメインに参加させます。
端末は、CentOS8で。

必要なパッケージをインストールします。

# dnf -y install realmd \
                 sssd \
                 oddjob \
                 oddjob-mkhomedir \
                 adcli \
                 samba-common-tools \
                 krb5-workstation

ドメインが検索できるかチェック。

# realm discover LAB.LOCAL
lab.local
  type: kerberos
  realm-name: LAB.LOCAL
  domain-name: lab.local
  configured: no
  server-software: active-directory
  client-software: sssd
  required-package: oddjob
  required-package: oddjob-mkhomedir
  required-package: sssd
  required-package: adcli
  required-package: samba-common-tools

ドメイン参加。Administrator で参加を実行。

# realm join -U Administrator LAB.LOCAL
Administrator に対するパスワード:
# id Administrator@lab.local
uid=1571000500(administrator@lab.local) gid=1571000513(domain users@lab.local) groups=1571000513(domain users@lab.local),1571000519(enterprise admins@lab.local),1571000512(domain admins@lab.local),1571000518(schema admins@lab.local),1571000520(group policy creator owners@lab.local),1571000572(denied rodc password replication group@lab.local)

ログインIDにドメイン名省略(use_fully_qualified_names を False)、/homeに作られるホームディレクトリもドメイン名省略する(fallback_homedir を /home/%u)ように設定を修正。

# vi /etc/sssd/sssd.conf
# cat /etc/sssd/sssd.conf
[sssd]
domains = lab.local
config_file_version = 2
services = nss, pam

[domain/lab.local]
ad_domain = lab.local
krb5_realm = LAB.LOCAL
realmd_tags = manages-system joined-with-adcli
cache_credentials = True
id_provider = ad
krb5_store_password_if_offline = True
default_shell = /bin/bash
ldap_id_mapping = True
use_fully_qualified_names = False
fallback_homedir = /home/%u
access_provider = ad
# systemctl restart sssd


ドメインのユーザで、sudo コマンドを使えるように細工する。
方法としては、ドメインに sudo を利用できるグループを作成し、ユーザを所属させる。
Linuxには、wheelグループがあるが、作成したグループの ID が異なるため、wheel グループを作成して所属しても、sudo コマンドは利用できない。
そこで、visudo コマンドを実行し、作成したグループを追記して、利用できるようにする。
以下のコマンドを dc.lab.local で実行して、グループ作成

# samba-tool group add "sudousers"
Added group sudousers

以下のコマンドを Linuxクライアントで実行して、sudo グループ 設定を実施。

# echo "%sudousers ALL=(ALL) ALL" | EDITOR='tee -a' visudo >/dev/null

ドメイン参加(Windows)

Windows クライアントを作成したドメインに参加させます。
コンピュータ名が登録されるため、ドメイン参加前に端末のコンピュータ名を変更しておきます。

  1. 「システムのプロパティ」の「変更」ボタンをクリック
  2. "ドメイン" を選択
  3. ドメイン名 "lab.local" を入力して「OK」ボタンをクリック (※この際、"lab.local" が検索できる必要があります。DNS サーバのアドレスを DC に設定しておきます。)
  4. IDは、"LAB\Administrator"。パスワードは、ドメイン作成時に設定したパスワードを入力します。
  5. 成功した場合、ようこそのメッセージボックスがでるので、「OK」ボタンをクリック。
  6. 設定画面を「OK」ボタンをクリックして閉じて、端末再起動


ログイン画面が出たら、"LAB\Administrator"で、パスワードは、ドメイン作成時に設定したパスワードを入力してログイン。
これで、ドメイン参加は完了。

ドメインのユーザ管理とかを楽にするために、以下の GUI ツールをインストールします。
インストール方法とかは、サイトのページを参照してください。
インストール後、スタートメニュー -「Windows 管理ツール」-「Active Directory ユーザーとコンピュータ」 から、ユーザーの追加とか行えます。

RSAT(Remote Server Administration Tools for Windows 10)

なお、通常作ったユーザだと、10台までコンピュータの参加が可能なので、上限を撤廃する必要があるみたい。
ADSI エディター を使って、ms-DS-MachineAccountQuota の値 10 を 0 に変更する。

参考:Windows Server 2016に、ドメイン参加できない(コンピュータが登録されずエラー) (Qiita)

f:id:tomikuma_white_bear:20200402211826p:plain
ADSI エディター

また追加したユーザは、いろいろ制限があるので、設定をする。

DC(Domain Controller)作成

DC(Domain Controller) は、最近だとSamba4でできるみたい。
なので、DC 用の仮想マシンを CentOS8 + Samba4 で、構築してみる。

仮想マシンを作って、CentOS8 をインストール(最小構成のインストールで、最低限のセットアップ)。
ホスト名やIP、ゲートウェイなどは、構想図に合わせて、設定を実施。
CentOS8 からは yum から dnf になっているようなので、dnf でパッケージの更新。

# dnf -y upgrade

ログイン時、Failed to set locale, defaulting to C のようなエラーが出る場合、言語パックが入っていないためにでるみたい。
日本語でセットアップして、コンソールが英語のみの場合、英語の言語パックをインストールすればOK

# dnf -y install langpacks-en

NTP で時刻合わせするために、CentOS8 の NTP クライアントの chrony をインストール。
NTP の個別設定が必要な場合は、chrony.conf を編集してください。

# dnf -y install chrony
# vi /etc/chrony.conf
# systemctl restart chronyd
# systemctl status chronyd
# chronyc sources

Samba4 インストールして、DC を構築します。
CentOS8 の Samba パッケージには、必要なコマンドとかがないので、ソースからビルドする必要があります。
ビルド準備として、ビルドに必要なパッケージをインストール。

参考: Samba Wiki

# dnf -y install bind-utils
# dnf -y install dnf-plugins-core
# dnf -y install epel-release
# dnf -y config-manager --set-enabled PowerTools
# dnf -y update
# dnf -y install docbook-style-xsl gcc gdb gnutls-devel gpgme-devel jansson-devel \
      keyutils-libs-devel krb5-workstation libacl-devel libaio-devel \
      libarchive-devel libattr-devel libblkid-devel libtasn1 libtasn1-tools \
      libxml2-devel libxslt lmdb-devel openldap-devel pam-devel perl \
      perl-ExtUtils-MakeMaker perl-Parse-Yapp popt-devel python3-cryptography \
      python3-dns python3-gpg python36-devel readline-devel rpcgen systemd-devel \
      tar zlib-devel cups-devel
# dnf clean all

Samba ソースをダウンロードして、ビルド。
configure にオプションを追加して、systemd で制御する service ファイルを作成するようにします。
また、service ファイルが systemd が認識する場所に格納されないので、コピーして、自動起動できるようにしておく。
(configure のオプションで解決できると思うが。。。)
有効は、smb.conf がないので、エラーになるから後で。

# curl -OL https://download.samba.org/pub/samba/samba-latest.tar.gz
# tar zxvf samba-latest.tar.gz
# cd samba-4.12.0/
# ./configure --with-systemd --systemd-install-services
# make
# make install
# cp /usr/local/samba/lib/systemd/system/smb.service /usr/lib/systemd/system/
# cp /usr/local/samba/lib/systemd/system/samba.service /usr/lib/systemd/system/
# cp /usr/local/samba/lib/systemd/system/nmb.service /usr/lib/systemd/system/
# cp /usr/local/samba/lib/systemd/system/winbind.service /usr/lib/systemd/system/
# systemctl daemon-reload
# systemctl list-unit-files --type=service

.bash_profile の PATH に、Samba のパスを設定。

# vi ~/.bash_profile
# cat ~/.bash_profile
if [ -f ~/.bashrc ]; then
        . ~/.bashrc
fi
PATH=/usr/local/samba/bin:/usr/local/samba/sbin:$PATH:$HOME/bin

export PATH
# source ~/.bash_profile
# samba -V

Firewall に設定を追加して、DC としてのポートを許可する。

# firewall-cmd --add-port={53,88,389,464}/{tcp,udp} --permanent
# firewall-cmd --add-port={135,139,445,636}/tcp --permanent
# firewall-cmd --add-port=137-138/udp --permanent
# firewall-cmd --add-port=1024-5000/tcp --permanent
# firewall-cmd --add-port=3268-3269/tcp --permanent
# firewall-cmd --add-port=49152-65535/tcp --permanent
# firewall-cmd --reload

SELinux を permissive に。
SELinux の設定を正しくすれば、enforcing のままでも動作するはず。

# vi /etc/selinux/config
# cat /etc/selinux/config
SELINUX=permissive
SELINUXTYPE=targeted
# reboot

Samba-tool を実行して、ドメイン設定を行う。
基本は、デフォルトで OK なので、Enter で。

# samba-tool domain provision --use-rfc2307 --interactive
Realm [LAB.LOCAL]:
Domain [LAB]:
Server Role (dc, member, standalone) [dc]:
DNS backend (SAMBA_INTERNAL, BIND9_FLATFILE, BIND9_DLZ, NONE) [SAMBA_INTERNAL]: 
DNS forwarder IP address (write 'none' to disable forwarding) [192.168.50.254]:
Administrator password:
Retype password:
# systemctl start samba
# systemctl enable samba

Samba 動作確認。

# smbclient -L localhost -U%

        Sharename       Type      Comment
        ---------       ----      -------
        sysvol          Disk
        netlogon        Disk
        IPC$            IPC       IPC Service (Samba 4.12.0)
SMB1 disabled -- no workgroup available

# smbclient //localhost/netlogon -UAdministrator -c 'ls'
Enter LAB\Administrator's password:
  .                                   D        0  Wed Apr  1 21:58:55 2020
  ..                                  D        0  Wed Apr  1 21:59:01 2020

                52403200 blocks of size 1024. 49538124 blocks available

DC が DNS になるので、DNS 設定を書き換え。
nmtui で、NICDNS サーバーを DC サーバーの IP アドレス、ドメイン検索に、"lab.local" を設定する。
また、ルータの DHCP サーバーで配布する DNS サーバーアドレスも、この DC サーバーの IP アドレスに変えておくとよいかも。

# nmtui
# reboot
# cat /etc/resolv.conf
search lab.local
nameserver 192.168.200.1

DNS 動作確認

# samba-tool dns zonelist 127.0.0.1 -U Administrator
# host dc.lab.local
dc.lab.local has address 192.168.200.1
# host -t SRV _ldap._tcp.lab.local
_ldap._tcp.lab.local has SRV record 0 100 389 dc.lab.local.
# host -t SRV _kerberos._tcp.lab.local
_kerberos._tcp.lab.local has SRV record 0 100 88 dc.lab.local.
# host www.yahoo.co.jp
www.yahoo.co.jp is an alias for edge12.g.yimg.jp.
edge12.g.yimg.jp has address 182.22.28.252

Kerberos 認証関係の設定。

# mv /etc/krb5.conf krb5.conf.org
# cp -p /usr/local/samba/private/krb5.conf /etc/krb5.conf
# cat /usr/local/samba/private/krb5.conf
[libdefaults]
        default_realm = LAB.LOCAL
        dns_lookup_realm = false
        dns_lookup_kdc = true

[realms]
LAB.LOCAL = {
        default_domain = lab.local
}

[domain_realm]
        dc = LAB.LOCAL

# kinit Administrator@LAB.LOCAL
Password for Administrator@LAB.LOCAL:
Warning: Your password will expire in 41 days on 2020年05月13日 21時59分01秒

# klist
Ticket cache: FILE:/tmp/krb5cc_0
Default principal: Administrator@LAB.LOCAL

Valid starting       Expires              Service principal
2020-04-01T22:13:30  2020-04-02T08:13:30  krbtgt/LAB.LOCAL@LAB.LOCAL
        renew until 2020-04-02T22:13:26

ユーザのパスワードポリシー設定。

複雑性OFF
# samba-tool domain passwordsettings set --complexity=off
パスワード長 8文字以上
# samba-tool domain passwordsettings set --min-pwd-length=8
パスワード有効期間 無期限
# samba-tool domain passwordsettings set --max-pwd-age=0
# samba-tool domain passwordsettings set --min-pwd-age=0
確認
# samba-tool domain passwordsettings show
Password information for domain 'DC=lab,DC=local'

Password complexity: off
Store plaintext passwords: off
Password history length: 24
Minimum password length: 8
Minimum password age (days): 0
Maximum password age (days): 0
Account lockout duration (mins): 30
Account lockout threshold (attempts): 0
Reset account lockout after (mins): 30

ここまでくれば、DC として動くようになっているはず!

ルータ構築

ルータの OS として、Linux とか Windows でも、よいのだが、今回は軽量な vyOS にしてみた。 (VM ホストは、ちょっと豪華な PC なので、なるべく軽く。。。)

ISOの入手や、推奨マシンスペック、インストール手順などは、以下を参考にして実施。

VyOS jp (wiki.vyos-users.jp)

ルータ マシン用意

192.168.200.0/24 のネットワークと、ルータの VM(512MB のメモリと 2GB のストレージ) を用意。

vyOS インストール

まずISO を入手して、CD ブートを実施。
そして、ログイン。(ID, パスワードは、起動画面に明記されている)

次のコマンドを実行し、質問に答えれば、インストールできる。

# install image

終わったら、再起動して、HDD からブートする。

# reboot

設定

日本語配列キーボード設定
キーボードの記号類が正しく入力されないので、キーマップを変更し、正しく入力できるようにする。
Vyatta/VyOSで利用するキーボードを日本語配列キーボードにする を参考にして、変更する。 ファイルを直接編集する場合は、root にならないと保存できない。 以下のコマンドで root になれる。

# sudo -i

以降の設定をする場合は、編集モードに移行して行う。

# configure
# set interfaces ethernet eth0 address 192.168.50.10/24
# set interfaces ethernet eth0 description 'Out-network'
# set interfaces ethernet eth1 address 192.168.200.254/24
# set interfaces ethernet eth1 description 'Inner-network'
# set protocols static route 0.0.0.0/0 next-hop 192.168.50.254 distance 200

system に設定することも可能。 ただし、内側の Network が 2つ以上ある場合、内側同士のルーティングがデフォルトゲートウェイに流れてしまって、うまく動かない可能性あり。 この形式で指定して、distance で正しくルーティングするようにする。

# set protocols static route 192.168.200.0/24 next-hop 192.168.200.253 distance 1
# set service dhcp-server shared-network-name eth1 subnet 192.168.200.0/24 default-router 192.168.200.254
# set service dhcp-server shared-network-name eth1 subnet 192.168.200.0/24 dns-server 192.168.50.254
# set service dhcp-server shared-network-name eth1 subnet 192.168.200.0/24 domain-name lab.local
# set service dhcp-server shared-network-name eth1 subnet 192.168.200.0/24 start 192.168.200.128 stop 192.168.200.253

192.168.200.128/24 ~ 192.168.200.253 を DHCP で割り当てる。

# set service dns forwarding cache-size 0
# set service dns forwarding listen-on eth1
# set service dns forwarding system

192.168.200.254 に来た DNS 向け要求を転送

# set service ssh port 22
# set system host-name router.lab.local
# set system name-server 192.168.50.254
# set system time-zone Asia/Tokyo
# show
# commit
# save

CI/CD トライ!

CI/CD やってみようかなってことで、自宅マシンに、VM 立てて、そこでやってみよう! せっかくだから、認証とかも、DC(Domain Controller) 立てて一括管理とかチャレンジしてみるか。

って軽い感じで、やってみる!

そんなわけで、ざっくりネットワーク構成考えてみた。

f:id:tomikuma_white_bear:20200326212030p:plain
構想図

とりあえず、内部なんでルータのセキュリティはなしで!(笑) 相互にお互いのネットワーク行き来できるようにして、いろいろ遊べる環境に!

DC 立てて、各マシンをドメインに参加して、認証をすべて DC で。 GitLab 立てて、コミットすれば、各 GitLab Runner でビルド実行

まずは、ここまでの CI を目標にするか。

(後に ansible 使ってデプロイ、単体テスト実行まで行けたらいいな。。。)

CSV出力

Attributeで遊んでみようと、とりあえずCSVを出力するのをやってみようかと。
ってことで、作ってみた。

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
 
namespace CSVWriter
{
    public class CsvWriter
    {
        #region 内部利用クラス
        /// <summary>
        /// CSVヘッダー情報
        /// </summary>
        protected class CsvHeaderInfo
        {
            #region Property
            /// <summary>
            /// 表示ラベル
            /// </summary>
            public string Label { get; set; }
            /// <summary>
            /// プロパティ名
            /// </summary>
            public string PropertyName { get; set; }
            #endregion
 
            /// <summary>
            /// コンストラクタ
            /// </summary>
            public CsvHeaderInfo() { }
            /// <summary>
            /// コンストラクタ
            /// </summary>
            /// <param name="label">表示ラベル</param>
            public CsvHeaderInfo(string label)
            {
                Label = label;
                PropertyName = label;
            }
        }
 
        /// <summary>
        /// CSV形式の文字を生成する
        /// </summary>
        protected class CsvDataFormatter : List<string>
        {
            /// <summary>
            /// データを設定する
            /// </summary>
            /// <param name="data">データ</param>
            /// <returns>データを追加した自身</returns>
            public CsvDataFormatter SetData(IEnumerable<string> data)
            {
                this.Clear();
                this.AddRange(data);
                return this;
            }
 
            /// <summary>
            /// CSVの文字をエスケープする
            /// </summary>
            /// <param name="data">エスケープする文字列</param>
            /// <returns>エスケープした文字列</returns>
            private string Escape(string data)
            {
                if (string.IsNullOrEmpty(data))
                {
                    return "";
                }
                int index = data.IndexOfAny(new char[] { '\n', ',', '"' }, 0);
                if (index != -1)
                {
                    data = string.Format("\"{0}\"", data.Replace("\"", "\"\""));
                }
                return data;
            }
 
            /// <summary>
            /// データをカンマ区切り文字列へ変換する
            /// </summary>
            /// <returns>カンマ区切り文字列</returns>
            public override string ToString()
            {
                return string.Join(",", this.Select(item => Escape(item)));
            }
        }
        #endregion
 
        protected List<CsvHeaderInfo> GetHeaderInfo(Type t)
        {
            var tergetHeaders = t.GetProperties().Where(prop =>
            {
                return prop.GetCustomAttributes(typeof(CsvIgnoreAttribute), false).Count() == 0;
            }).Select(prop =>
            {
                CsvHeaderInfo data = new CsvHeaderInfo(prop.Name);
                CsvHeaderAttribute attr = Attribute.GetCustomAttribute(prop, typeof(CsvHeaderAttribute)) as CsvHeaderAttribute;
                if (attr != null)
                {
                    data.Label = attr.Label;
                }
                return data;
            });
 
            List<CsvHeaderInfo> ret = new List<CsvHeaderInfo>();
            ret.AddRange(tergetHeaders);
            return ret;
        }
 
        /// <summary>
        /// CSVファイルに出力する
        /// </summary>
        /// <param name="fileName">出力するファイル名</param>
        /// <param name="data">出力するデータ</param>
        /// <param name="outLabel">ラベルを出力するかどうかのフラグ</param>
        public void Write(string fileName, object[] data, bool outLabel = true)
        {
            StreamWriter sw = null;
            try
            {
                sw = new StreamWriter(fileName, false, Encoding.GetEncoding("shift_jis"));
                Write(sw, data, outLabel);
            }
            finally
            {
                if (sw != null)
                {
                    sw.Close();
                }
            }
        }
 
        /// <summary>
        /// ストリームにCSVデータを出力する
        /// </summary>
        /// <param name="fileName">出力するストリーム</param>
        /// <param name="data">出力するデータ</param>
        /// <param name="outLabel">ラベルを出力するかどうかのフラグ</param>
        public void Write(StreamWriter sw, object[] data, bool outLabel = true)
        {
            Type t = data[0].GetType();
            List<CsvHeaderInfo> header = GetHeaderInfo(t);
 
            CsvDataFormatter formatter = new CsvDataFormatter();
            if (outLabel)
            {
                // Output Label.
                string headerData = formatter.SetData(header.Select(p => p.Label)).ToString();
                if (!string.IsNullOrEmpty(headerData))
                {
                    sw.WriteLine(headerData);
                }
            }
 
            // Output Data
            foreach (object one in data)
            {
                string oneLine = formatter.SetData(header.Select(p =>
                {
                    PropertyInfo prop = t.GetProperty(p.PropertyName);
                    if (prop != null)
                    {
                        return prop.GetValue(one, null).ToString();
                    }
                    return "";
                })).ToString();
                if (!string.IsNullOrEmpty(oneLine))
                {
                    sw.WriteLine(oneLine);
                }
            }
        }
    }
 
    #region Attribute
    /// <summary>
    /// CSVとして出力する際の属性
    /// </summary>
    [AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = false)]
    public class CsvHeaderAttribute : Attribute
    {
        #region Property
        /// <summary>
        /// ラベル情報
        /// </summary>
        public string Label { get; set; }
        #endregion
 
        /// <summary>
        /// コンストラクタ
        /// </summary>
        /// <param name="label">ラベル情報</param>
        public CsvHeaderAttribute(string label)
        {
            Label = label;
        }
    }
 
    /// <summary>
    /// CSVとして出力する際の無視属性
    /// </summary>
    [AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = false)]
    public class CsvIgnoreAttribute : Attribute
    {
    }
    #endregion
}

出力するデータのクラスを作成しておいて、作成したクラスの配列を渡せば、プロパティの内容をそのまま出力する様になっています。
行頭にラベルをありつけることもでき、CsvHeaderAttribute属性をプロパティに設定すれば、その値がラベルとして出力されます。
また、CsvIgnoreAttribute属性をプロパティに設定すれば出力されないように制御できます。

使用例はこんな感じ。

class FruitData
{
    [CsvHeader("果物の名前")]
    public string Name { get; set; }
    [CsvHeader("値段")]
    public int Price { get; set; }
    [CsvIgnore()]
    public string Shop { get; set; }
}
 
class Program
{
    static void Main(string[] args)
    {
        FruitData[] data = new[]
        {
            new FruitData()
            {
                Name = "リンゴ",
                Price = 120,
                Shop = "A商店"
            },
            new FruitData()
            {
                Name = "オレンジ",
                Price = 100,
                Shop = "B商店"
           },
           new FruitData()
           {
               Name = "リンゴ",
               Price = 150,
               Shop = "B商店"
           }
        };
 
        CsvWriter writer = new CsvWriter();
        writer.Write("Shopping.csv", data);
    }
}

出力されたCSVはこんな感じ。

果物の名前,値段
リンゴ,120
オレンジ,100
リンゴ,150

DebuggerHidden属性

デバッガの抑制を行います。
動作的には、DebuggerStepThroughAttributeと同じように動作します。

ただし、DebuggerStepThroughAttributeは、メソッド内にブレークポイントを設定すれば止まりますが、DebuggerHiddenAttributeの場合は、設定しても止まりません。

[DebuggerHidden]
static void Print()
{
    Debug.WriteLine("Print Method");
}