本記事の内容は筆者個人の見解であり、所属組織・企業の公式見解ではありません。また、本記事の原案はAIを用いて作成しており、掲載しているコードおよび手順については筆者自身が検証・確認しています。
全体アーキテクチャのイメージ
本記事で扱う構成はざっくり次のイメージです。
-
Keeperボルト上に、テスト用Webアプリのログイン情報(ユーザー名/パスワード/TOTPシークレット)を1つのレコードとして保存
-
Pythonコードから
- (パターンA)Keeperコマンダー SDK を使ってユーザーログイン → レコード取得
- (パターンB)Keeper Secrets Manager(KSM)を使ってアプリケーションとしてレコード取得
-
取得したシークレットを 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"
}
実運用では password や device_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)の方がより適しています。
まとめ
ポイントを整理すると、
- Selenium自体は「渡された値を入力するだけ」なので、シークレットの保管場所は外部に切り出すべき
- Keeperボルトにログイン情報やTOTPシークレットをまとめて保存しておくことで、ローテーションや権限管理が一元化できる
- 自動化やCI/CDで使う場合は、ユーザー資格情報ではなく Keeper Secrets Manager を利用するのが望ましい
といったところです。