---
# System prepended metadata

title: '`tj_tsai.ldif` (個人資料)'
tags: [tj_tsai, ldif, LDAP]

---

`tj_tsai.ldif` (個人資料)
===
###### tags: `LDAP`
###### tags: `LDAP`, `ldif`, `tj_tsai`

<br>

[TOC]

<br>

## 背景知識
- [[HackMD] LDAP 相關術語](/vW9loVLbQt6R7uMMcM5zrQ)
- [LDAP schema reference](https://docs.pingidentity.com/pingds/8/schemaref/preface.html)
    - [Object classes](https://docs.pingidentity.com/pingds/8/schemaref/object-classes.html)
    - [Attribute types](https://docs.pingidentity.com/pingds/8/schemaref/attribute-types.html)
- [[HackMD] ldapsearch 指令與相關參數](/YWXj3DaKT3SDV4Rq2OTPrw?both)
- [[HackMD] slappasswd](/GHd7jYgZTzexmpkkxOADeA)

<br>

:::info
## 個人資料
```
- dc:com
  - dc:google
    - dc:www
      - ou:ocis
        - ou:div3
          - ou:dep2
            - ou:sec1
              - uid:tj_tsai
```

- email: `tj_tsai@gmail.com`
- ext: 39981 (分機)
- ubuntu:
    - name: `tj_tsai`
    - uid: 10001
    - gid: 10001
:::

<br>

## 設計 `tj_tsai` 的 LDAP 資料
> 小提醒
>
> * 一般會用 `dc=google,dc=com` 當 Base DN；是否要多一層 `dc=www` 視你的命名政策而定。下面範例已照你的結構寫好。
> * `inetOrgPerson` 需要 `sn`（姓氏）；我先用 `tsai` 佔位。
> * 若 `gidNumber=10001` 的群組尚未存在，下面也一併建立一個 `posixGroup`。

---

### 步驟1：準備個人資料 `tj_tsai.ldif`

```ldif=
# 1) 階層（不存在時先建立；存在就略過）
dn: ou=ocis,dc=www,dc=google,dc=com
objectClass: organizationalUnit
ou: ocis

dn: ou=div3,ou=ocis,dc=www,dc=google,dc=com
objectClass: organizationalUnit
ou: div3

dn: ou=dep2,ou=div3,ou=ocis,dc=www,dc=google,dc=com
objectClass: organizationalUnit
ou: dep2

dn: ou=sec1,ou=dep2,ou=div3,ou=ocis,dc=www,dc=google,dc=com
objectClass: organizationalUnit
ou: sec1

# 2) 群組（可選：若 10001 尚未存在）
dn: cn=gid10001,ou=sec1,ou=dep2,ou=div3,ou=ocis,dc=www,dc=google,dc=com
objectClass: posixGroup
cn: gid10001
gidNumber: 10001
memberUid: tj_tsai

# 3) 使用者（建議用 uid 當 RDN；若要用 cn，將 dn 第一行改成 cn=tj_tsai,...）
dn: uid=tj_tsai,ou=sec1,ou=dep2,ou=div3,ou=ocis,dc=www,dc=google,dc=com
objectClass: inetOrgPerson
objectClass: posixAccount
objectClass: shadowAccount
cn: tj_tsai
sn: tsai
givenName: tj
displayName: tj_tsai
mail: tj_tsai@gmail.com
telephoneNumber: 39981
uid: tj_tsai
uidNumber: 10001
gidNumber: 10001
homeDirectory: /home/tj_tsai
loginShell: /bin/bash
gecos: tj_tsai
# 用 slappasswd 產生 SSHA 雜湊後，替換下一行
userPassword: {SSHA}rzc8Z7vrdQIOOiezOmmoJQxg6XnrZivI
# (userPassword 很重要，不是亂填的！)
# (-w 123456)
```
- ### userPassword 欄位填寫
    - ### 產生密碼雜湊方式
        > 背景知識：[[HackMD] slappasswd](/GHd7jYgZTzexmpkkxOADeA)

        ```bash
        slappasswd -h {SSHA} -s '你的強密碼'
        # 複製輸出（例如 {SSHA}8Hk...），貼到 LDIF 的 userPassword 欄位
        ```
        - **安裝方式**
            ```
            sudo apt install slapd -y
            # 輸入 admin 密碼：123456

            $ slappasswd -h {SSHA} -s 123456
            {SSHA}4khG9Pq9hDOF1MjqJLaOjTApKkDSyvMQ

            $ slappasswd -h {SSHA} -s 123456
            {SSHA}LrvHoHNWlKHiL8Wwdha/wQcQPvGw5qYi
            ```
            - 每次會產生不同的雜湊，不要緊，選擇任意一個雜湊來用，雜湊中會記錄 salt
            - 登錄時，明碼密碼會串接所選雜湊中的 salt，再使用 SHA1 產生雜湊，雜湊相同表示密碼正確
    - ### 在匯入資料後，可用底下方式驗證 tj_tsai 帳密
        ```
        # 在任何可以連到 openldap 
        $ ldapwhoami -x -H ldap://openldap.openldap \
            -D "uid=tj_tsai,ou=sec1,ou=dep2,ou=div3,ou=ocis,dc=www,dc=google,dc=com" \
            -w 123456
        
        # 成功回傳：dn:...
        dn:uid=tj_tsai,ou=sec1,ou=dep2,ou=div3,ou=ocis,dc=www,dc=google,dc=com
        
        # 失敗回傳：
        ldap_bind: Invalid credentials (49)
        ```

---

### 步驟2：[選用][前製作業] 建立指令測試環境
> 背景知識：
> - [[HackMD] 在 Kubernetes 上部署 OpenLDAP 的詳細教學](/4BzU5X-XQze7Z9IXg0JiAw)
> - [[HackMD] 在 Ubuntu 上安裝 OpenLDAP 的詳細教學](/JsLFQ_bvSQKvSO46ewWiyQ)

- ### 在 Kubernetes (K8s) 上
    - ### 有 root 的 pod
        ```bash
        kubectl run ububtu-2404 -it --rm --image=ubuntu:24.04 -- bash
        apt update

        # install ldap commands:
        # - ldapadd
        # - ldapcompare
        # - ldapdelete
        # - ldapexop
        # - ldapmodify
        # - ldapmodrdn
        # - ldappasswd
        # - ldapsearch
        # - ldapurl
        # - ldapwhoami
        apt install ldap-utils -y
        ```
    - ### 沒有 root 的 pod
        ```
        kubectl run ldap-test --rm -it --image=bitnami/openldap:latest -- bash
        ```
- ### 先測試「管理者」的帳密可以通
    ```bash
    # 範例，依實際
    ldapsearch -x -H ldap://openldap.openldap:389 \
      -D "cn=admin,dc=www,dc=google,dc=com" \
      -w YourSecurePassword \
      -b "dc=www,dc=google,dc=com" -LLL
    ```

---

### 匯入指令

> 管理者 DN 依你的資料庫 suffix 而定：
>
> * 若資料庫 suffix 是 `dc=google,dc=com`，用 `cn=admin,dc=google,dc=com`
> * 若另開一個 suffix `dc=www,dc=google,dc=com`，則用 `cn=admin,dc=www,dc=google,dc=com`

```bash
ldapadd -x -D "cn=admin,dc=google,dc=com" -W -f tj_tsai.ldif
```
- ### 執行結果
    ```
    adding new entry "ou=ocis,dc=www,dc=google,dc=com"
    adding new entry "ou=div3,ou=ocis,dc=www,dc=google,dc=com"
    adding new entry "ou=dep2,ou=div3,ou=ocis,dc=www,dc=google,dc=com"
    adding new entry "ou=sec1,ou=dep2,ou=div3,ou=ocis,dc=www,dc=google,dc=com"
    adding new entry "cn=gid10001,ou=sec1,ou=dep2,ou=div3,ou=ocis,dc=www,dc=google,dc=com"
    adding new entry "uid=tj_tsai,ou=sec1,ou=dep2,ou=div3,ou=ocis,dc=www,dc=google,dc=com"
    ```

---

### 用指令驗證
- ### 用 uid 找人
    ```bash
    # 預設本地端主機，會連 localhost:389
    ldapsearch -x -b "dc=www,dc=google,dc=com" "(uid=tj_tsai)"
    # 若是本機端無 LDAP 服務，則會有底下錯誤：
    # ldap_sasl_bind(SIMPLE): Can't contact LDAP server (-1)
    
    # 其他主機 / 容器 / Pod 上查 (假設需帳密)
    ldapsearch -x -H ldap://openldap.openldap:389 \
      -D "cn=admin,dc=www,dc=google,dc=com" \
      -w "YourSecurePassword" \
      -b "dc=www,dc=google,dc=com" \
      "(uid=tj_tsai)" \
      -LLL
    ```
    - ### 執行結果 (攜帶 `-LLL`)
        ```ldif=
        dn: uid=tj_tsai,ou=sec1,ou=dep2,ou=div3,ou=ocis,dc=www,dc=google,dc=com
        objectClass: inetOrgPerson
        objectClass: posixAccount
        objectClass: shadowAccount
        objectClass: top
        cn: tj_tsai
        sn: tsai
        givenName: tj
        displayName: tj_tsai
        mail: tj_tsai@gmail.com
        telephoneNumber: 29981
        uid: tj_tsai
        uidNumber: 10001
        gidNumber: 10001
        homeDirectory: /home/tj_tsai
        loginShell: /bin/bash
        userPassword:: e1NTSEF9UkVQTEFDRV9XSVRIX0hBU0g=
        gecos: tj_tsai
        ```
        - **`userPassword` 欄位**
            - `::` 表示 base64 編碼
            - base64 解碼方式
                ```
                $ echo "e1NTSEF9UkVQTEFDRV9XSVRIX0hBU0g=" | base64 -d
                {SSHA}REPLACE_WITH_HASH
                ```
    - ### ldapsearch 不帶 `-D` & `-w` 管理者帳密
        回傳結果不包含 `userPassword` 欄位
        
- ### 看 Unix 必備屬性（uidNumber/gidNumber/homeDirectory/loginShell）
    ```
    # 預設本地端主機，會連 localhost:389
    ldapsearch -x -b "uid=tj_tsai,ou=sec1,ou=dep2,ou=div3,ou=ocis,dc=www,dc=google,dc=com"
    
    # 其他主機 / 容器 / Pod 上查 (假設需帳密)
    ldapsearch -x -H ldap://openldap.openldap:389 \
      -D "cn=admin,dc=www,dc=google,dc=com" \
      -w "YourSecurePassword" \
      -b "uid=tj_tsai,ou=sec1,ou=dep2,ou=div3,ou=ocis,dc=www,dc=google,dc=com" \
      -LLL
    ```
    - ### 執行結果 (不攜帶 `-LLL` 參數)
        ```ldif=
        # extended LDIF
        #
        # LDAPv3
        # base <uid=tj_tsai,ou=sec1,ou=dep2,ou=div3,ou=ocis,dc=www,dc=google,dc=com> with scope subtree
        # filter: (objectclass=*)
        # requesting: ALL
        #

        # tj_tsai, sec1, dep2, div3, ocis, www.google.com
        dn: uid=tj_tsai,ou=sec1,ou=dep2,ou=div3,ou=ocis,dc=www,dc=google,dc=com
        objectClass: inetOrgPerson
        objectClass: posixAccount
        objectClass: shadowAccount
        objectClass: top
        cn: tj_tsai
        sn: tsai
        givenName: tj
        displayName: tj_tsai
        mail: tj_tsai@gmail.com
        telephoneNumber: 29981
        uid: tj_tsai
        uidNumber: 10001
        gidNumber: 10001
        homeDirectory: /home/tj_tsai
        loginShell: /bin/bash
        userPassword:: e1NTSEF9UkVQTEFDRV9XSVRIX0hBU0g=
        gecos: tj_tsai

        # search result
        search: 2
        result: 0 Success

        # numResponses: 2
        # numEntries: 1
        ```
    - ### 執行結果 (攜帶 `-LLL` 參數)
        ```ldif=
        dn: uid=tj_tsai,ou=sec1,ou=dep2,ou=div3,ou=ocis,dc=www,dc=google,dc=com
        objectClass: inetOrgPerson
        objectClass: posixAccount
        objectClass: shadowAccount
        objectClass: top
        cn: tj_tsai
        sn: tsai
        givenName: tj
        displayName: tj_tsai
        mail: tj_tsai@gmail.com
        telephoneNumber: 29981
        uid: tj_tsai
        uidNumber: 10001
        gidNumber: 10001
        homeDirectory: /home/tj_tsai
        loginShell: /bin/bash
        userPassword:: e1NTSEF9UkVQTEFDRV9XSVRIX0hBU0g=
        gecos: tj_tsai
        ```
    - ### ldapsearch 不帶 `-D` & `-w` 管理者帳密
        回傳結果不包含 `userPassword` 欄位

<br>

### [UI] 透過 phpLDAPadmin 匯入
![](https://hackmd.io/_uploads/SkDWdHIsle.png)

<br>

### [UI] 透過 phpLDAPadmin 查看
- `cn=gid10001`
    ![](https://hackmd.io/_uploads/ryGrhr8oee.png)
- `uid=tj_tsai`
    ![](https://hackmd.io/_uploads/SyDmfIUolx.png)
    ---
    ![](https://hackmd.io/_uploads/HJqVfIUoeg.png)



<br>

---

## 常見錯誤訊息

### Object class violation
Could not add the object to the LDAP server.

- ### Error
    - **LDAP said**: Object class violation
    - **Error number**: 0x41 (LDAP_OBJECT_CLASS_VIOLATION)
    - **Description**:
        You tried to perform an operation that would cause an undefined attribute to exist or that would remove a required attribute, given the current list of ObjectClasses. This can also occur if you do not specify a structural objectClass when creating an entry, or if you specify more than one structural objectClass.

- ### gpt-5 (2025/10/15)
    ```ldif=
    dn: uid=tj_tsai,ou=users,ou=ocis,dc=www,dc=google,dc=com
    objectClass: posixAccount
    cn: TJ Tsai
    uid: tj_tsai
    uidNumber: 10000
    gidNumber: 10000
    homeDirectory: /home/tj_tsai
    loginShell: /bin/bash
    userPassword: {SSHA}w2XNVRJUmQjanSsYMJ6cphGbw194M/k+
    ```
    你這筆條目只有 `objectClass: posixAccount`，而你的伺服器套的是 **RFC2307bis** 架構：
    `posixAccount` 在這個架構是 **AUXILIARY（輔助類別）**，不是結構類別。LDAP 每個條目**一定要有且只有一個 STRUCTURAL（結構）objectClass**，所以會報：

    > `LDAP_OBJECT_CLASS_VIOLATION (0x41)`（沒有結構類別 / 或缺必要屬性）

    有兩種常見修法，擇一使用即可：

    ---

    ### 方案 A（最常用）：inetOrgPerson + posixAccount (+ shadowAccount)

    `inetOrgPerson` 是 **STRUCTURAL**，需要 `sn`（姓氏）與 `cn`。

    ```ldif
    dn: uid=tj_tsai,ou=users,ou=ocis,dc=www,dc=google,dc=com
    objectClass: inetOrgPerson
    objectClass: posixAccount
    objectClass: shadowAccount
    cn: TJ Tsai
    sn: Tsai
    givenName: TJ
    uid: tj_tsai
    uidNumber: 10000
    gidNumber: 10000
    homeDirectory: /home/tj_tsai
    loginShell: /bin/bash
    userPassword: {SSHA}w2XNVRJUmQjanSsYMJ6cphGbw194M/k+
    ```

    > 若你不需要個人資訊欄位，也可省 `givenName`、`mail` 等。

    ---

    ### 方案 B（極簡帳號）：account + posixAccount (+ shadowAccount)

    `account` 是 **STRUCTURAL**，必備 `uid`。

    ```ldif
    dn: uid=tj_tsai,ou=users,ou=ocis,dc=www,dc=google,dc=com
    objectClass: account
    objectClass: posixAccount
    objectClass: shadowAccount
    uid: tj_tsai
    cn: TJ Tsai
    uidNumber: 10000
    gidNumber: 10000
    homeDirectory: /home/tj_tsai
    loginShell: /bin/bash
    userPassword: {SSHA}w2XNVRJUmQjanSsYMJ6cphGbw194M/k+
    ```

    ---

    ### 建議再補一個對應群組（可選，但實務常見）

    ```ldif
    dn: cn=gid10000,ou=groups,ou=ocis,dc=www,dc=google,dc=com
    objectClass: posixGroup
    cn: gid10000
    gidNumber: 10000
    memberUid: tj_tsai
    ```

    ---

    ### 匯入與驗證

    ```bash
    # 匯入
    ldapadd -x -H ldap://openldap.openldap \
      -D "cn=admin,dc=www,dc=google,dc=com" -w 'YourSecurePassword' \
      -f user.ldif

    # 驗證
    ldapsearch -x -H ldap://openldap.openldap \
      -b "dc=www,dc=google,dc=com" "(uid=tj_tsai)" -LLL
    ```

    ### 為什麼你會遇到這個錯誤？

    * 你的 OpenLDAP 採用的 schema 把 `posixAccount` 定義為 **AUXILIARY**；
    * 條目沒有 STRUCTURAL 類別（如 `inetOrgPerson` 或 `account`），因此違反 objectClass 規則。

    照上面兩個方案其一調整後，匯入就會過。



<br>

---

## 討論

### gecos 欄位用途為何？
- ### gpt-5 (2025/09/16)
    簡單說：**`gecos` 是用來放「使用者的真實姓名與聯絡備註」的欄位**。
    在 Linux 的 `/etc/passwd` 第 5 欄就是 GECOS；在 LDAP 的 `posixAccount` 也常見同名屬性，用來對應這個資訊。

    ### 為什麼需要它？

    * 讓系統或工具顯示人類可讀的名稱（例如 `finger`、顯示登入提示、GUI 使用者清單）。
    * 可附帶聯絡資訊（歷史上用 **逗號 ,** 分成幾段）：
      1. 全名（最常用）
      2. 辦公室/房號
      3. 公司電話
      4. 住家電話
      5. 其他備註
    * 不影響登入/權限，**純描述用**；安全上別放敏感資訊。

    ### 注意事項

    * `/etc/passwd` 用 **冒號 :** 分欄，**GECOS 裡不可包含冒號或換行**；逗號可用來分子欄位。
    * 現代目錄常把正式資料放在 `cn`、`displayName`、`telephoneNumber`、`roomNumber` 等屬性；`gecos` 多半只是為了與傳統 `passwd` 對應。

    ### 範例

    **/etc/passwd** 一行（第 5 欄為 GECOS）：

    ```
    alice:x:1001:1001:Alice Lee,Room 123,02-2345-6789,,:/home/alice:/bin/bash
    ```

    **LDAP（posixAccount）**：

    ```
    dn: uid=alice,ou=users,dc=example,dc=com
    objectClass: inetOrgPerson
    objectClass: posixAccount
    cn: Alice Lee
    sn: Lee
    uid: alice
    uidNumber: 1001
    gidNumber: 1001
    homeDirectory: /home/alice
    loginShell: /bin/bash
    gecos: Alice Lee,Room 123,02-2345-6789,,
    ```

    ### 常用操作

    * **查看（本機）**：

        ```bash
        getent passwd alice | cut -d: -f5
        # 或
        finger alice   # 若 finger 已安裝
        ```

    * **設定/修改（本機）**：

        ```bash
        sudo usermod -c "Alice Lee,Room 123,02-2345-6789,," alice
        # 或互動式
        sudo chfn alice
        ```

    * **新增使用者時帶入（Debian/Ubuntu 的 adduser）**：

        ```bash
        sudo adduser --gecos "Alice Lee,Room 123,02-2345-6789,," alice
        ```

    * **與 LDAP 整合（nsswitch/SSSD）時**：`gecos` 會被映射成 `/etc/passwd` 的第 5 欄給應用程式讀取；正式欄位仍建議用 `cn`、`displayName`、`telephoneNumber` 等作權威資料。

    <br>
    
    ### [彩蛋] GECOS 原名
    - **GECOS** 的原名是 **General Electric Comprehensive Operating Supervisor**（通用電氣綜合作業監督系統）。
    - 後來該系統改名為 **GCOS = General Comprehensive Operating System**。
    - Linux `/etc/passwd` 裡的 `gecos` 欄位就是承襲這段歷史命名，但如今僅作「使用者資訊（全名/備註）」之用。

<br>

---

<br>

### shadowAccount 用途又是為何？
> https://docs.pingidentity.com/pingds/8/schemaref/oc-shadowAccount.html
- ### Question
    ```
    OID：1.3.6.1.1.1.2.1
    Names：shadowAccount
    Description：Additional attributes for shadow passwords
    Superior classes：top
    Class type：AUXILIARY: for use in augmenting attributes of entries that already have a structural object class.
    ```
    shadowAccount 用途又是為何？
    
- ### gpt-5 (2025/09/16)
    下面這句話先記住就好：
    **`shadowAccount` 是一個「輔助 (auxiliary) 物件類別」用來把 `/etc/shadow` 的「密碼到期/老化」欄位搬到 LDAP 裡集中管理**（RFC 2307，OID `1.3.6.1.1.1.2.1`）。它通常會跟 `posixAccount`、`inetOrgPerson` 一起掛在同一個使用者條目上。

    ### 它解決什麼？

    * 讓 Linux 用戶端（NSS/SSSD/PAM）能從 LDAP 讀到與 `/etc/shadow` 相同語意的欄位，進而套用：密碼最短/最長使用天數、到期警告、到期後寬限期、帳號到期日…等規則。
    * 適合要把登入帳號統一由 LDAP 發佈，又希望保留傳統 shadow 密碼老化行為的環境。

    ### 主要屬性與意義（單位除特別標註外皆為「天」）

    | 屬性                 | 用途                                                               |
    | ------------------ | ---------------------------------------------------------------- |
    | `shadowLastChange` | 上次更改密碼的日期（**自 1970-01-01 起算的天數**）。                               |
    | `shadowMin`        | 兩次變更密碼間的**最少天數**（未達則拒絕改密碼）。                                      |
    | `shadowMax`        | 密碼**最長有效天數**（超過即視為過期）。                                           |
    | `shadowWarning`    | 密碼將到期前**提前警告天數**。                                                |
    | `shadowInactive`   | 密碼過期後，**再過幾天**就鎖定帳號（未登入即失效）。                                     |
    | `shadowExpire`     | **帳號到期日**（同樣是自 1970-01-01 起算的天數；空值/未設表示不限制）。                     |
    | `shadowFlag`       | 保留欄位，通常設 0。                                                      |
    | `userPassword`     | 多數實作允許放在此（雜湊值，如 `{SSHA}`、`{SHA512-CRYPT}`）；也常放在 `inetOrgPerson`。 |

    > 小提醒：`shadowLastChange`/`shadowExpire` 用「天數自 Epoch 起算」，換算指令例：
    > `date -d "1970-01-01 + <天數> days" +%F`

    ### 為什麼是 AUXILIARY？

    `shadowAccount` 不是結構類別（non-structural），**不能單獨當一筆使用者條目的骨幹**，而是**用來「加掛」** 到已經有結構類別（如 `inetOrgPerson` 或 `posixAccount`）的條目上，補上 shadow 欄位。

    ### 範例 LDIF

    ```ldif
    dn: uid=alice,ou=users,dc=example,dc=com
    objectClass: inetOrgPerson
    objectClass: posixAccount
    objectClass: shadowAccount
    cn: Alice Lee
    sn: Lee
    uid: alice
    uidNumber: 1001
    gidNumber: 1001
    homeDirectory: /home/alice
    loginShell: /bin/bash
    userPassword: {SSHA}A1B2C3...
    shadowLastChange: 19990
    shadowMin: 0
    shadowMax: 90
    shadowWarning: 7
    shadowInactive: 14
    shadowExpire: 21000
    shadowFlag: 0
    ```

    ### 在用戶端如何生效？

    * 使用 **SSSD** 或 **nslcd** 搭配 PAM（`pam_unix`/`pam_sss`），客戶端會讀取這些屬性來判斷密碼是否到期、是否可更改、是否應提示警告等。
    * 一些指令（如 `chage -l alice`）在使用 LDAP 時也能顯示這些資訊（取決於你用的 NSS/SSSD 與 PAM 設定）。

    ### 常見注意事項

    * **單位是「天」且自 1970-01-01 起算**，不是秒；別混用。
    * 若同時採用 OpenLDAP 的 **ppolicy**（密碼政策 overlay），要決定以哪套為準，避免規則打架（SSSD 有 `ldap_pwexpire_policy = shadow` 等設定可選擇行為）。
    * 未設定 `shadow*` 時，通常視為**不啟用老化/到期限制**（依各 PAM/NSS 實作預設）。

    需要我幫你把你現有的 LDAP 使用者條目補上合適的 `shadow*` 參數（例如企業常見：`Max=90, Warning=7, Inactive=14`）的範例值嗎？

<br>

---

<br>

### shadowAccount 參數意義
> https://docs.pingidentity.com/pingds/8/schemaref/oc-shadowAccount.html
- ### Question

    簡單說明底下參數意義：
    shadowMin: 0
    shadowMax: 90
    shadowWarning: 7
    shadowInactive: 14
    shadowExpire: 21000
    shadowFlag: 0
    
- ### gpt-5 (2025/09/16)

    以下皆以「天」為單位，除 `shadowExpire` 外都以**上次改密碼日 (`shadowLastChange`)** 為基準：

    * **shadowMin: 0**
      上次改密碼後最少等待 0 天即可再次改密碼（= 隨時可改）。
    * **shadowMax: 90**
      密碼最長有效 90 天；超過即視為過期，需更改。
    * **shadowWarning: 7**
      密碼到期前 7 天開始提醒使用者更改。
    * **shadowInactive: 14**
      密碼過期後再過 14 天仍未更改，帳號即被鎖定（停用登入）。
    * **shadowExpire: 21000**
      帳號「絕對到期日」，自 1970-01-01 起算第 21000 天，即 **2027-07-01**，到這天起帳號失效（與密碼是否過期無關）。
    * **shadowFlag: 0**
      保留欄位，通常固定為 0（不使用）。


<br>

---

<br>

### `userPassword: {SSHA}REPLACE_WITH_HASH` 的疑惑
- ### Question
    ```
    # 用 slappasswd 產生 SSHA 雜湊後，替換下一行
    userPassword: {SSHA}REPLACE_WITH_HASH
    ```
    `"{SSHA}REPLACE_WITH_HASH"` 就是我使用的密碼
    一定要 slappasswd 工具產生，才能當密碼嗎？

- ### 背景知識
    - [[HackMD] slappasswd](/GHd7jYgZTzexmpkkxOADeA)

- ### gpt-5 (2025/09/24)
    不是一定要用 `slappasswd`，但你目前的用法有個關鍵誤會：

    * `-w` **永遠填「原始明碼密碼」**，不是 `{SSHA}...`，也不是 base64。
    * `userPassword` 裡的 `{SSHA}REPLACE_WITH_HASH` 在教學裡只是**佔位字串**。它不是合法雜湊，更不是可拿來登入的密碼。

    要讓登入成功，你有三種做法，擇一即可：

    ---

    ## 做法 A（最省事，推薦）：用 `ldappasswd` 讓伺服器自動雜湊

    這樣你**只要給明碼**，伺服器會依設定（通常是 `{SSHA}` 或你設定的 `olcPasswordHash`）自動轉成雜湊存進 `userPassword`。

    ```bash
    # 以管理者改使用者密碼（伺服器自動雜湊）
    ldappasswd -x -H ldap://10.78.26.241:389 \
      -D "cn=admin,dc=www,dc=google,dc=com" -w "12" \
      "uid=tj_tsai,ou=sec1,ou=dep2,ou=div3,ou=ocis,dc=www,dc=google,dc=com" \
      -s 'MyS3cret!'

    # 立即測試（注意：-w 是明碼）
    ldapwhoami -x -H ldap://10.78.26.241:389 \
      -D "uid=tj_tsai,ou=sec1,ou=dep2,ou=div3,ou=ocis,dc=www,dc=google,dc=com" \
      -w 'MyS3cret!'
    ```

    > 若你想指定雜湊演算法（如 `{SSHA}` 或 `{SSHA512}`），在 `cn=config` 設 `olcPasswordHash` 後再用 `ldappasswd` 設密碼即可。

    ---

    ## 做法 B：客戶端先產生合法雜湊，再用 `ldapmodify` 寫入

    可以用 `slappasswd`（最簡單），或自己用任意語言算出 **SHA1(密碼+salt) 再附上 salt**，然後 base64；最後組成 `{SSHA}<base64>` 字串。

    ```bash
    HASH=$(slappasswd -h {SSHA} -s 'MyS3cret!')   # 會回類似 {SSHA}6y0... 一長串
    cat > update-password.ldif <<EOF
    dn: uid=tj_tsai,ou=sec1,ou=dep2,ou=div3,ou=ocis,dc=www,dc=google,dc=com
    changetype: modify
    replace: userPassword
    userPassword: $HASH
    EOF

    ldapmodify -x -H ldap://10.78.26.241:389 \
      -D "cn=admin,dc=www,dc=google,dc=com" -w "12" \
      -f update-password.ldif
    ```

    接著用明碼 `MyS3cret!` 測試 bind（跟上面一樣）。

    ---

    ## 做法 C（僅測試用，不安全）：直接存明碼

    你也可以**直接存明碼**到 `userPassword`，這樣登入就用那個明碼。之後再改成雜湊。

    ```ldif
    dn: uid=tj_tsai,ou=sec1,ou=dep2,ou=div3,ou=ocis,dc=www,dc=google,dc=com
    changetype: modify
    replace: userPassword
    userPassword: mycleartext
    ```

    ```bash
    ldapmodify -x -H ldap://10.78.26.241:389 \
      -D "cn=admin,dc=www,dc=google,dc=com" -w "12" \
      -f clear.ldif

    ldapwhoami -x -H ldap://10.78.26.241:389 \
      -D "uid=tj_tsai,ou=sec1,ou=dep2,ou=div3,ou=ocis,dc=www,dc=google,dc=com" \
      -w 'mycleartext'
    ```

    ---

    ### 你原本的狀況為什麼會失敗？

    * `userPassword:: e1NTSEF9UkVQTEFDRV9XSVRIX0hBU0g=` 是 **ldapsearch 把值 base64 顯示** 的結果；解碼後是字串 `{SSHA}REPLACE_WITH_HASH`，表示你真的把**佔位字串**存進去了。
    * slapd 看到字首 `{SSHA}`，會把它當成「雜湊值」，但後面不是合法的 base64（對應到雜湊+salt 的位元組），所以**比對一定失敗**。
    * 你拿 `{SSHA}REPLACE_WITH_HASH` 或它的 base64 去當密碼也不會成功——**可登入的永遠是原始明碼**（例如 `MyS3cret!`），而不是雜湊字串本身。

    > 進階：如果你真的「想把字面上包含 `{SSHA}` 的字串當密碼」（不建議），可存成 `{CLEARTEXT}{SSHA}REPLACE_WITH_HASH` 才會被當作純文字處理；但實務上別這麼做。

    需要的話，我可以幫你檢查目前 slapd 的 `olcPasswordHash` 與（可選的）`ppolicy` 設定，順便給一份一次到位的調整指令。

<br>

---

<br>

### 如果要定義很多人，一般要如何規劃 ldif 資料？
- ### gpt-5 (2025/09/12)
    下面整理一份「多使用者」LDIF 規劃做法＋範本，讓你能穩定擴充、版本控管、且便於維運（大量建立／修改／加群組）。

    ### 規劃原則（精華版）

    1. **DIT 佈局**

       * 建議**扁平放人**：`ou=people,dc=google,dc=com`（或你的 `dc=www,dc=google,dc=com`），避免把部門層級寫進 DN，改用屬性紀錄（如 `departmentNumber: div3/dep2/sec1`）。
       * 群組獨立：`ou=groups,...`（Unix 用 `posixGroup`，系統/應用可用 `groupOfNames`）。
    2. **RDN 命名**

       * 以 **`uid=<login>`** 作 RDN 最穩（例：`uid=tj_tsai,ou=people,...`），比 `cn=` 不易變動。
    3. **物件類別**

       * 一般：`inetOrgPerson`（人資/通訊錄屬性）。
       * Unix/Linux 整合：加 `posixAccount`、`shadowAccount`。
    4. **必要屬性最小集**

       * `uid, cn, sn, mail, uidNumber, gidNumber, homeDirectory, loginShell, userPassword`
       * 擴充：`telephoneNumber`（分機）、`departmentNumber`、`manager`（DN）、`employeeNumber` 等。
    5. **UID/GID 發號策略**

       * 制定規則（如從 10000 起連號），**單一來源**維護（CSV/DB），LDIF 由此產生，避免衝突。
    6. **群組策略**

       * Unix 權限用 `posixGroup` 的 `gidNumber`、`memberUid`；
       * App 權限用 `groupOfNames` 的 `member: <使用者 DN>`；**不要以 OU 當權限依據**。
    7. **檔案切割與順序**

       * `00_base_ou.ldif`（基礎 OU）→ `10_groups.ldif`（群組/ gid）→ `20_people_*.ldif`（人員）→ `30_membership.ldif`（加人進群組）。
       * 一次匯多檔，比一個大檔更好維運與除錯。
    8. **變更管理**

       * 新增：內容紀錄（預設 `add`）。
       * 更新/加群組：用 **Change Record**（`changetype: modify`）。
       * 調部門：若 DN 不含部門就**不需改 DN**；只改 `departmentNumber` 即可。
       * DN 必須改時（真的用到多層 OU）：`changetype: modrdn`。
    9. **版本控管與回滾**

       * LDIF 全部進 Git；重大操作前 `slapcat` 備份，可回滾。
    10. **密碼與政策**

    * 密碼用 `slappasswd` 產生雜湊（SSHA/BCRYPT）；
    * 可啟用 `ppolicy`（密碼規則、到期、鎖定）—策略由 LDIF 佈署。

    ---

    ### 檔案結構（建議）

    ```
    ldap/
      00_base_ou.ldif
      10_groups.ldif
      20_people_div3.ldif
      20_people_div5.ldif
      30_membership.ldif
    ```

    #### 00\_base\_ou.ldif（基礎樹）

    > 若你堅持 `dc=www,dc=google,dc=com`，把下方 `dc=google,dc=com` 改成你的 suffix 即可。

    ```ldif
    dn: ou=people,dc=google,dc=com
    objectClass: organizationalUnit
    ou: people

    dn: ou=groups,dc=google,dc=com
    objectClass: organizationalUnit
    ou: groups
    ```

    #### 10\_groups.ldif（群組與 gid）

    > 建立 Unix 群組（`posixGroup`）與應用群組（`groupOfNames`）。

    ```ldif
    # Unix 群組：tn，gidNumber 10001
    dn: cn=tn,ou=groups,dc=google,dc=com
    objectClass: posixGroup
    cn: tn
    gidNumber: 10001

    # App 群組（可選）：data-science
    dn: cn=data-science,ou=groups,dc=google,dc=com
    objectClass: groupOfNames
    cn: data-science
    member: uid=admin1,ou=people,dc=google,dc=com  # 先放管理者；之後再補成員
    ```

    #### 20\_people\_div3.ldif（人員批次）

    > 扁平放在 `ou=people`，用屬性標記部門／單位。

    ```ldif
    # tj_tsai
    dn: uid=tj_tsai,ou=people,dc=google,dc=com
    objectClass: inetOrgPerson
    objectClass: posixAccount
    objectClass: shadowAccount
    uid: tj_tsai
    cn: TJ Tsai
    sn: Tsai
    givenName: TJ
    mail: tj_tsai@google.com
    telephoneNumber: 39981
    departmentNumber: ocis/div3/dep2/sec1
    uidNumber: 10001
    gidNumber: 10001
    homeDirectory: /home/tj_tsai
    loginShell: /bin/bash
    gecos: TJ Tsai
    userPassword: {SSHA}REPLACE_WITH_HASH

    # 另一位（示例）
    dn: uid=alice,ou=people,dc=google,dc=com
    objectClass: inetOrgPerson
    objectClass: posixAccount
    objectClass: shadowAccount
    uid: alice
    cn: Alice Chen
    sn: Chen
    givenName: Alice
    mail: alice@google.com
    telephoneNumber: 29888
    departmentNumber: ocis/div3/dep2/sec1
    uidNumber: 10002
    gidNumber: 10001
    homeDirectory: /home/alice
    loginShell: /bin/bash
    gecos: Alice Chen
    userPassword: {SSHA}REPLACE_WITH_HASH
    ```

    #### 30\_membership.ldif（把人加進群組）

    > 用 **變更記錄**，可重複執行（具可重放性，維運友善）。

    ```ldif
    # 加入 Unix 群組（posixGroup）
    dn: cn=tn,ou=groups,dc=google,dc=com
    changetype: modify
    add: memberUid
    memberUid: tj_tsai
    memberUid: alice
    -
    #（若已存在會報錯，可改用 replace 或分行單獨 add）

    # 加入應用群組（groupOfNames）
    dn: cn=data-science,ou=groups,dc=google,dc=com
    changetype: modify
    add: member
    member: uid=tj_tsai,ou=people,dc=google,dc=com
    member: uid=alice,ou=people,dc=google,dc=com
    ```

    ---

    ### 匯入／更新流程

    ```bash
    # 先建 OU / 群組 / 人員，再補群組成員
    ldapadd    -x -D "cn=admin,dc=google,dc=com" -W -f ldap/00_base_ou.ldif
    ldapadd    -x -D "cn=admin,dc=google,dc=com" -W -f ldap/10_groups.ldif
    ldapadd    -x -D "cn=admin,dc=google,dc=com" -W -f ldap/20_people_div3.ldif

    # 變更（加成員）
    ldapmodify -x -D "cn=admin,dc=google,dc=com" -W -f ldap/30_membership.ldif
    ```

    ---

    ### 批次產生 LDIF（建議流程）

    1. **單一資料源**（CSV/Excel/DB）：維護 `uid, cn, sn, givenName, mail, telephoneNumber, uidNumber, gidNumber, departmentNumber, loginShell` 等欄。
    2. **模板化**：用 Jinja2/ERB 產生 LDIF（避免手刻）。
    3. **雜湊密碼**：先批次產生 `{SSHA}` 並放入欄位或改為初次登入強制改密碼流程。
    4. **CI 驗證**：匯入前跑靜態檢查（欄位完整性、id 唯一性、DN 合法）。

    簡易範例（概念示意，非必要）：

    ```jinja2
    {# people.ldif.j2 #}
    {% for u in users -%}
    dn: uid={{u.uid}},ou=people,dc=google,dc=com
    objectClass: inetOrgPerson
    objectClass: posixAccount
    objectClass: shadowAccount
    uid: {{u.uid}}
    cn: {{u.cn}}
    sn: {{u.sn}}
    givenName: {{u.givenName}}
    mail: {{u.mail}}
    telephoneNumber: {{u.ext}}
    departmentNumber: {{u.dept}}
    uidNumber: {{u.uidNumber}}
    gidNumber: {{u.gidNumber}}
    homeDirectory: /home/{{u.uid}}
    loginShell: /bin/bash
    gecos: {{u.cn}}
    userPassword: {{u.password_hash}}

    {% endfor -%}
    ```

    ---

    ### 驗證查詢（抽樣檢核）

    ```bash
    # 抓某 OU 所有人（若扁平即抓 ou=people）
    ldapsearch -x -b "ou=people,dc=google,dc=com" "(objectClass=inetOrgPerson)" uid mail uidNumber gidNumber

    # 看群組成員（posixGroup）
    ldapsearch -x -b "ou=groups,dc=google,dc=com" "(cn=tn)" cn gidNumber memberUid

    # 看 App 群組（groupOfNames）
    ldapsearch -x -b "ou=groups,dc=google,dc=com" "(cn=data-science)" cn member
    ```

    ---

    ### 小結

    * **人放 `ou=people`、權限看群組**是大規模目錄最省事的規劃。
    * **RDN 用 `uid=`**，避免人事異動牽動 DN。
    * **CSV/模板→LDIF→Git 版本控管**，搭配 `ldapadd/ldapmodify`，即可安全批次上線。
    * **變更用 change-record**，可重放、好追溯。

    如果你決定維持目前的 `dc=www,dc=google,dc=com` 與深層部門 OU，也可以——建議仍然把人放 `ou=people`，把 `ocis/div3/dep2/sec1` 寫進 `departmentNumber` 或自定屬性即可，遷調就不會牽動 DN。需要我把上面範本換成你的 `dc=www,dc=google,dc=com` 版，我可以直接幫你改好。

<br>

---

<br>

### UPG：每位使用者各有一個群組
> i.e. `gidNumber == uidNumber`、`cn == uid`
> by gpt-5 (2025/10/18)

- ### UPG
    User Private Group（使用者私有群組）
- ### LDAP 條目（每人一個 UPG）
    - ### 群組（posixGroup）
        ```ldif=
        dn: cn=<uid>,ou=groups,ou=ocis,dc=google,dc=com
        objectClass: top
        objectClass: posixGroup
        cn: <uid>
        gidNumber: <同 uidNumber>
        memberUid: <uid>
        ```
        
    - ### 使用者（posixAccount）
        ```ldif=
        dn: uid=<uid>,ou=users,ou=ocis,dc=asus,dc=com
        objectClass: top
        objectClass: person
        objectClass: organizationalPerson
        objectClass: inetOrgPerson
        objectClass: posixAccount
        uid: <uid>
        cn: <Real Name>
        sn: <Surname>
        uidNumber: <唯一號, 例如 10001>
        gidNumber: <同 uidNumber>
        homeDirectory: /home/<uid>
        loginShell: /bin/bash
        userPassword: {SSHA}REPLACE_WITH_HASH
        ```

- ### 實際範例
    ```ldif=
    # === UPG for tj_tsai ===
    dn: cn=tj_tsai,ou=groups,ou=ocis,dc=google,dc=com
    objectClass: top
    objectClass: posixGroup
    cn: tj_tsai
    gidNumber: 10001
    memberUid: tj_tsai

    # === user tj_tsai ===
    dn: uid=tj_tsai,ou=users,ou=ocis,dc=google,dc=com
    objectClass: top
    objectClass: person
    objectClass: organizationalPerson
    objectClass: inetOrgPerson
    objectClass: posixAccount
    cn: TJ Tsai
    sn: Tsai
    uid: tj_tsai
    uidNumber: 10001
    gidNumber: 10001
    homeDirectory: /home/tj_tsai
    loginShell: /bin/bash
    userPassword: {SSHA}REPLACE_WITH_HASH
    ```

<br>

{%hackmd vaaMgNRPS4KGJDSFG0ZE0w %}