0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

SeleniumとKeeperボルトの連携で安全にシークレットを扱う(お試し編)

Posted at

本記事の内容は筆者個人の見解であり、所属組織・企業の公式見解ではありません。また、本記事の原案はAIを用いて作成しており、掲載しているコードおよび手順については筆者自身が検証・確認しています。

全体アーキテクチャのイメージ

本記事で扱う構成はざっくり次のイメージです。

  1. Keeperボルト上に、テスト用Webアプリのログイン情報(ユーザー名/パスワード/TOTPシークレット)を1つのレコードとして保存

  2. Pythonコードから

    • (パターンA)Keeperコマンダー SDK を使ってユーザーログイン → レコード取得
    • (パターンB)Keeper Secrets Manager(KSM)を使ってアプリケーションとしてレコード取得
  3. 取得したシークレットを Selenium に渡して、

    • ログイン画面のユーザー名/パスワード入力
    • 二要素認証(TOTP)のコード入力

Selenium側から見ると、単に「変数に入ってきた値をsend_keysするだけ」なので、テストコードからはシークレットの保管場所を意識しなくてよい構成になります。


前提環境

本記事では以下の環境を想定します。

  • OS: Windows 11(他OSでも概ね同様)

  • Python: 3.11 〜 3.13

  • ブラウザ: Google Chrome

  • Keeper関連

    • Keeper 企業アカウント
    • Keeperコマンダー SDK(Python)
    • Keeper Secrets Manager アプリケーション(後半のKSMパートで使用)

Pythonパッケージとしては、以下を利用します。

pip install selenium
pip install keepersdk                    # Keeperコマンダー SDK(Python)
pip install keeper-secrets-manager-core  # Keeper Secrets Manager Python SDK
pip install pyotp                        # TOTP生成用

Keeperボルト側の準備

1. テスト用ログインレコードの作成

まず、Keeperボルト上にテスト用のログインレコードを1件作成しておきます。このレコードにはテストで使用するユーザー名(またはログインID)、パスワード、TOTP用のワンタイムコードが格納されている想定とし、以降のサンプルコードではそのレコードUID(例: AbCdEfGhIjKlMnOpQrStUv)を指定して、これら3つの値を取得します。

2. KSM用アプリケーションの作成(パターンBで使用)

Keeper Secrets Manager を使う場合は、管理者コンソールで Secrets Manager アプリケーションを新規作成し、対象のレコード(または含まれる共有フォルダ)をアプリケーションに付与したうえで、アプリケーション設定画面からクライアント設定 config.json をダウンロードし、今回のPythonスクリプトと同じディレクトリに配置しておきます。


パターンA: Keeperコマンダー SDK からレコードを取得して Selenium に渡す

まずはユーザーとしてKeeperにログインするパターンです。

1. .keeper/config.json の作成

公式ドキュメントのサンプルをベースに、最低限以下のような構成を用意します。

.keeper/config.json のサンプル
{
  "users": [
    {
      "user": "your.email@example.com",
      "password": "",           
      "server": "keepersecurity.jp",
      "last_device": {
        "device_token": ""
      }
    }
  ],
  "servers": [
    {
      "server": "keepersecurity.jp",
      "server_key_id": 10
    }
  ],
  "last_login": "your.email@example.com",
  "last_server": "keepersecurity.jp"
}

実運用では passworddevice_token を空のままにしておき、マスターパスワードや2FAコードは Python 実行時に入力する運用が望ましいですが、本記事のパターンAでは検証を簡単にするため、password にマスターパスワードを保存しておき、実行時には 2FA コードのみ手動入力する前提とします。

2. Python側で Keeperコマンダー SDK を利用してレコードを取得

以下は、Keeperコマンダー SDK と Selenium を組み合わせて、Keeperボルト上のログインレコードから資格情報を取得し、そのままHubSpotにログインする、実際に使用した1ファイルのサンプルコードです。

パターンAのサンプルコード(Keeperコマンダー SDK + Selenium)
import sqlite3
import getpass
import pyotp

from keepersdk.authentication import login_auth, configuration, endpoint
from keepersdk.vault import sqlite_storage, vault_online, vault_record
from keepersdk.errors import KeeperApiError

from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

# ----------------------------------------------------------------------
# Config
# ----------------------------------------------------------------------
RECORD_UID = "AbCdEfGhIjKlMnOpQrStUv"  # Keeperボルト上のログインレコード UID
HUBSPOT_LOGIN_URL = "https://app.hubspot.com/login/legacy"


# ----------------------------------------------------------------------
# Keeper: fetch username, password, TOTP from record UID
# ----------------------------------------------------------------------
def get_keeper_credentials():
    print("[Keeper] Loading config.json and starting login...")

    config_storage = configuration.JsonConfigurationStorage()
    keeper_endpoint = endpoint.KeeperEndpoint(config_storage)
    login_ctx = login_auth.LoginAuth(keeper_endpoint)

    cfg = config_storage.get()
    user_cfg = cfg.users()[0]

    # マスターパスワードは config.json に保存しても、空にして実行時に入力してもよい
    login_ctx.login(user_cfg.username, user_cfg.password)

    while not login_ctx.login_step.is_final():
        step = login_ctx.login_step

        if isinstance(step, login_auth.LoginStepDeviceApproval):
            print("[Keeper] Device approval required – sending push...")
            step.send_push(login_auth.DeviceApprovalChannel.KeeperPush)
            input("[Keeper] Approve this device in Keeper, then press Enter...")

        elif isinstance(step, login_auth.LoginStepPassword):
            pwd = getpass.getpass("[Keeper] Enter Keeper master password: ")
            step.verify_password(pwd)

        elif isinstance(step, login_auth.LoginStepTwoFactor):
            channels = step.get_channels()
            channel = channels[0]
            # Loop until a valid 2FA code is entered
            while True:
                code = input(
                    f"[Keeper] Enter 2FA code for {channel.channel_name}: "
                ).strip()
                try:
                    step.send_code(channel.channel_uid, code)
                    break
                except KeeperApiError as e:
                    print(f"[Keeper] 2FA error ({e}). Please try again.")

        else:
            raise NotImplementedError(f"Unhandled login step: {type(step)}")

    if not isinstance(login_ctx.login_step, login_auth.LoginStepConnected):
        raise RuntimeError("[Keeper] Login failed")

    keeper_auth = login_ctx.login_step.take_keeper_auth()
    print("[Keeper] Login successful, syncing vault...")

    # In-memory SQLite vault
    conn = sqlite3.Connection("file::memory:", uri=True)
    vault_storage = sqlite_storage.SqliteVaultStorage(
        lambda: conn,
        vault_owner=bytes(keeper_auth.auth_context.username, "utf-8"),
    )
    v = vault_online.VaultOnline(keeper_auth, vault_storage)
    v.sync_down()

    # Find record by UID
    rec_meta = next(
        (r for r in v.vault_data.records() if r.record_uid == RECORD_UID),
        None,
    )
    if not rec_meta:
        v.close()
        keeper_auth.close()
        raise RuntimeError(f"[Keeper] Record UID {RECORD_UID} not found")

    rec = v.vault_data.load_record(rec_meta.record_uid)

    username = password = None
    otp_url = None

    if isinstance(rec, vault_record.PasswordRecord):
        username = rec.login
        password = rec.password
        otp_url = getattr(rec, "totp", None)

    elif isinstance(rec, vault_record.TypedRecord):
        login_field = rec.get_typed_field("login")
        if login_field:
            username = login_field.get_default_value(str)

        password_field = rec.get_typed_field("password")
        if password_field:
            password = password_field.get_default_value(str)

        otp_field = (
            rec.get_typed_field("oneTimeCode")
            or rec.get_typed_field("otp")
        )
        if otp_field:
            otp_url = otp_field.get_default_value(str)

    else:
        v.close()
        keeper_auth.close()
        raise RuntimeError(f"[Keeper] Unsupported record type: {type(rec)}")

    v.close()
    keeper_auth.close()

    if not username or not password:
        raise RuntimeError(
            f"[Keeper] Record {RECORD_UID} missing login/password fields"
        )

    totp_code = None
    if otp_url:
        totp = pyotp.parse_uri(otp_url)
        totp_code = totp.now()

    print("[Keeper] Retrieved username/password and TOTP from record.")
    return username, password, totp_code


# ----------------------------------------------------------------------
# Selenium: use those credentials to log into HubSpot
# ----------------------------------------------------------------------
def login_hubspot_with_keeper():
    username, password, totp_code = get_keeper_credentials()

    print(f"[Selenium] Got credentials. TOTP code (debug): {totp_code}")  # デバッグ用。実運用ではログ出力しないことを推奨
    print("[Selenium] Launching Chrome...")
    driver = webdriver.Chrome()
    wait = WebDriverWait(driver, 60)

    try:
        print(f"[Selenium] Opening {HUBSPOT_LOGIN_URL} ...")
        driver.get(HUBSPOT_LOGIN_URL)

        # ---- Step 1: email + password ----
        email_input = wait.until(
            EC.visibility_of_element_located(
                (By.CSS_SELECTOR, "input[type='email']")
            )
        )
        password_input = wait.until(
            EC.visibility_of_element_located(
                (By.CSS_SELECTOR, "input[type='password']")
            )
        )

        email_input.clear()
        email_input.send_keys(username)
        password_input.clear()
        password_input.send_keys(password)

        login_button = wait.until(
            EC.element_to_be_clickable(
                (By.CSS_SELECTOR, "button[type='submit']")
            )
        )
        print("[Selenium] Submitting username and password...")
        login_button.click()

        # ---- Step 2: MFA (Authenticator) page ----
        if totp_code:
            print("[Selenium] Waiting for MFA input field on two-factor page...")

            # Use the specific selector for the "Enter code" field.
            # If this ever breaks, re-inspect the input and adjust "#code".
            otp_input = wait.until(
                EC.visibility_of_element_located(
                    (By.CSS_SELECTOR, "#code")
                )
            )

            otp_input.clear()
            otp_input.send_keys(totp_code)
            print("[Selenium] Filled MFA code, submitting...")

            otp_submit = wait.until(
                EC.element_to_be_clickable(
                    (By.CSS_SELECTOR, "button[type='submit']")
                )
            )
            otp_submit.click()
        else:
            print("[Selenium] No TOTP code available; MFA step will require manual entry.")

        print("[Selenium] Login flow finished. Check the browser window.")
        input("Press Enter here to close the browser...")

    finally:
        driver.quit()
        print("[Selenium] Browser closed.")


# ----------------------------------------------------------------------
# Entry point
# ----------------------------------------------------------------------
if __name__ == "__main__":
    print("=== HubSpot login via Keeper + Selenium ===")
    login_hubspot_with_keeper()

パターンB: Keeper Secrets Manager(KSM)からレコードを取得して Selenium に渡す

次に、Keeper Secrets Manager(KSM)を使うパターンです。

こちらはユーザーのマスターパスワードや2FAではなく、「KSMアプリケーション」として認証する方式で、

  • CI/CD パイプライン
  • コンテナ環境
  • Selenium Grid などの自動化インフラ

から利用する場合に適しています。

1. KSMクライアント設定ファイルの配置

管理コンソールで作成した Secrets Manager アプリケーションから、config.json をダウンロードし、先ほどの Python スクリプトと同じディレクトリ(例: ksm/config.json)に配置します。

2. PythonコードでKSMからシークレットを取得

パターンBのサンプルコード(KSM + Selenium)
import os

from keeper_secrets_manager_core import SecretsManager
from keeper_secrets_manager_core.storage import FileKeyValueStorage
from keeper_secrets_manager_core.utils import get_totp_code

from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

# ----------------------------------------------------------------------
# Configuration
# ----------------------------------------------------------------------

# HubSpot login page
HUBSPOT_LOGIN_URL = "https://app.hubspot.com/login/legacy"

# Record UID in Keeper Secrets Manager that holds your HubSpot login
RECORD_UID = "AbCdEfGhIjKlMnOpQrStUv"

# Path to your KSM config file.
# This assumes the config file is named "config.json"
# and is in the same folder as this script (ksm/config.json).
KSM_CONFIG_PATH = os.path.join(os.path.dirname(__file__), "config.json")


# ----------------------------------------------------------------------
# KSM: Fetch username / password / TOTP from a record
# ----------------------------------------------------------------------

def get_credentials_from_ksm():
    """Read username, password and TOTP from Keeper Secrets Manager."""
    print("[KSM] Initialising Secrets Manager client...")

    # Prefer env var if set, otherwise fall back to config.json next to this script
    config_path = os.environ.get("KSM_CONFIG", KSM_CONFIG_PATH)

    if not os.path.exists(config_path):
        raise FileNotFoundError(
            f"[KSM] Config file not found: {config_path}. "
            f"Set KSM_CONFIG or place config.json next to this script."
        )

    secrets_manager = SecretsManager(
        config=FileKeyValueStorage(config_path)
    )
    print(f"[KSM] Using config: {config_path}")

    # Fetch the record by UID
    secrets = secrets_manager.get_secrets([RECORD_UID])
    if not secrets:
        raise RuntimeError(f"[KSM] No secrets returned for UID {RECORD_UID}")

    record = secrets[0]

    # Standard fields: login, password, oneTimeCode / otp
    username = record.field("login", single=True)
    password = record.field("password", single=True)

    # TOTP URL may be in oneTimeCode or otp field
    otp_url = record.field("oneTimeCode", single=True)
    if not otp_url:
        otp_url = record.field("otp", single=True)

    if not username or not password:
        raise RuntimeError(
            f"[KSM] Record {RECORD_UID} is missing login or password field."
        )

    totp_code = None
    if otp_url:
        # Use Keeper’s helper to turn otpauth:// URL into current code
        totp = get_totp_code(otp_url)
        totp_code = totp.code

    print("[KSM] Retrieved username/password and TOTP from KSM.")
    return username, password, totp_code


# ----------------------------------------------------------------------
# Selenium: Log into HubSpot using KSM credentials
# ----------------------------------------------------------------------

def login_hubspot_with_ksm():
    username, password, totp_code = get_credentials_from_ksm()
    print(f"[Selenium] Got credentials. TOTP code (debug): {totp_code}")  # デバッグ用。実運用ではログ出力しないことを推奨

    print("[Selenium] Launching Chrome...")
    driver = webdriver.Chrome()  # assumes chromedriver is on PATH
    wait = WebDriverWait(driver, 60)

    try:
        print(f"[Selenium] Opening {HUBSPOT_LOGIN_URL} ...")
        driver.get(HUBSPOT_LOGIN_URL)

        # ---- Step 1: email + password ----
        email_input = wait.until(
            EC.visibility_of_element_located(
                (By.CSS_SELECTOR, "input[type='email']")
            )
        )
        password_input = wait.until(
            EC.visibility_of_element_located(
                (By.CSS_SELECTOR, "input[type='password']")
            )
        )

        email_input.clear()
        email_input.send_keys(username)
        password_input.clear()
        password_input.send_keys(password)

        login_button = wait.until(
            EC.element_to_be_clickable(
                (By.CSS_SELECTOR, "button[type='submit']")
            )
        )
        print("[Selenium] Submitting username and password...")
        login_button.click()

        # ---- Step 2: MFA / two-factor page ----
        if totp_code:
            print("[Selenium] Waiting for MFA input field on two-factor page...")

            # HubSpot MFA "Enter code" input – selector based on inspection
            otp_input = wait.until(
                EC.visibility_of_element_located(
                    (By.CSS_SELECTOR, "#code")
                )
            )

            otp_input.clear()
            otp_input.send_keys(totp_code)
            print("[Selenium] Filled MFA code, submitting...")

            otp_submit = wait.until(
                EC.element_to_be_clickable(
                    (By.CSS_SELECTOR, "button[type='submit']")
                )
            )
            otp_submit.click()
        else:
            print("[Selenium] No TOTP code from KSM; please enter MFA manually.")

        print("[Selenium] Login flow finished. Check the browser window.")
        input("Press Enter here to close the browser...")

    finally:
        driver.quit()
        print("[Selenium] Browser closed.")


# ----------------------------------------------------------------------
# Entry point
# ----------------------------------------------------------------------

if __name__ == "__main__":
    print("=== HubSpot login via KSM + Selenium ===")
    login_hubspot_with_ksm()

どちらのパターンを使うべきか?

どちらのパターンを採用するかは用途と運用要件次第ですが、ローカルでの検証や個人開発のように「人がその場でログインできる」ケースでは、ユーザーのマスターパスワードと2FAを使ってログインするパターンA(Keeperコマンダー SDK)が手軽です。一方、Selenium テストを CI/CD やコンテナ、Selenium Grid などの環境で機械同士(machine-to-machine)で実行するユースケースでは、Secrets Managerアプリケーションとして認証するパターンB(KSM)の方がより適しています。


まとめ

ポイントを整理すると、

  1. Selenium自体は「渡された値を入力するだけ」なので、シークレットの保管場所は外部に切り出すべき
  2. Keeperボルトにログイン情報やTOTPシークレットをまとめて保存しておくことで、ローテーションや権限管理が一元化できる
  3. 自動化やCI/CDで使う場合は、ユーザー資格情報ではなく Keeper Secrets Manager を利用するのが望ましい

といったところです。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?