Refactor credential management: implement encryption and update file structure
Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
@@ -1,4 +0,0 @@
|
||||
[credentials]
|
||||
username=<username>
|
||||
password=<password>
|
||||
IPMIEK=0000000000000000000000000000000000000000
|
||||
@@ -7,7 +7,14 @@ import argparse
|
||||
import configparser
|
||||
import os
|
||||
|
||||
try:
|
||||
from cryptography.fernet import Fernet, InvalidToken
|
||||
except ImportError:
|
||||
sys.exit("Error: 'cryptography' package is required. Run: pip install cryptography")
|
||||
|
||||
DEFAULT_EK = "0000000000000000000000000000000000000000"
|
||||
DEFAULT_CREDENTIALS_FILE = "/root/cron_serverlists/credentials.ini"
|
||||
DEFAULT_KEYFILE_NAME = "secret.key"
|
||||
|
||||
# IPMI settings (loaded at runtime from credentials file)
|
||||
IPMIUSER = None
|
||||
@@ -65,7 +72,77 @@ def ts_print(msg):
|
||||
now = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
print(f"{now} {msg}")
|
||||
|
||||
def load_credentials(path):
|
||||
|
||||
def is_fernet_token(value):
|
||||
return isinstance(value, str) and value.startswith("gAAAA")
|
||||
|
||||
|
||||
def load_fernet(keyfile):
|
||||
if not os.path.exists(keyfile):
|
||||
raise RuntimeError(f"key file not found: {keyfile}")
|
||||
|
||||
with open(keyfile, "rb") as fh:
|
||||
key = fh.read()
|
||||
|
||||
try:
|
||||
return Fernet(key)
|
||||
except ValueError as e:
|
||||
raise RuntimeError(f"invalid key format in key file: {keyfile}") from e
|
||||
|
||||
|
||||
def decrypt_password(value, fernet):
|
||||
# Support both encrypted and plaintext credentials for smoother migrations.
|
||||
if not is_fernet_token(value):
|
||||
return value
|
||||
|
||||
if fernet is None:
|
||||
raise RuntimeError(
|
||||
"encrypted password detected but key file was not found. "
|
||||
"Use --keyfile /path/to/secret.key"
|
||||
)
|
||||
|
||||
try:
|
||||
return fernet.decrypt(value.encode()).decode()
|
||||
except InvalidToken as e:
|
||||
raise RuntimeError("failed to decrypt password; key file may not match credentials file") from e
|
||||
|
||||
|
||||
def resolve_credentials_path(cli_path):
|
||||
if cli_path and os.path.exists(cli_path):
|
||||
return cli_path
|
||||
|
||||
candidates = []
|
||||
if cli_path:
|
||||
candidates.append(cli_path)
|
||||
|
||||
candidates.append(os.path.join(os.getcwd(), "credentials.ini"))
|
||||
if DEFAULT_CREDENTIALS_FILE not in candidates:
|
||||
candidates.append(DEFAULT_CREDENTIALS_FILE)
|
||||
|
||||
for candidate in candidates:
|
||||
if os.path.exists(candidate):
|
||||
return candidate
|
||||
|
||||
raise RuntimeError("credentials file not found. Looked in: " + ", ".join(candidates))
|
||||
|
||||
|
||||
def resolve_keyfile(cli_keyfile, credentials_path):
|
||||
if cli_keyfile:
|
||||
return cli_keyfile if os.path.exists(cli_keyfile) else None
|
||||
|
||||
candidates = [
|
||||
os.path.join(os.path.dirname(credentials_path), DEFAULT_KEYFILE_NAME),
|
||||
os.path.join(os.path.dirname(DEFAULT_CREDENTIALS_FILE), DEFAULT_KEYFILE_NAME),
|
||||
]
|
||||
|
||||
for candidate in candidates:
|
||||
if os.path.exists(candidate):
|
||||
return candidate
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def load_credentials(path, fernet):
|
||||
if not os.path.exists(path):
|
||||
raise RuntimeError(f"credentials file not found: {path}")
|
||||
|
||||
@@ -73,9 +150,15 @@ def load_credentials(path):
|
||||
config.read(path)
|
||||
|
||||
try:
|
||||
username = config.get("credentials", "username", fallback="root")
|
||||
password = config.get("credentials", "password")
|
||||
encryption_key = config.get("credentials", "encryption_key", fallback=DEFAULT_EK)
|
||||
section = "ipmi_credentials"
|
||||
username = config.get(section, "username", fallback="root")
|
||||
encrypted_password = config.get(section, "password")
|
||||
password = decrypt_password(encrypted_password, fernet)
|
||||
encryption_key = config.get(
|
||||
section,
|
||||
"ipmiek",
|
||||
fallback=config.get(section, "encryption_key", fallback=DEFAULT_EK),
|
||||
)
|
||||
except (configparser.NoSectionError, configparser.NoOptionError) as e:
|
||||
raise RuntimeError(f"error reading credentials file: {e}")
|
||||
|
||||
@@ -150,9 +233,14 @@ def main():
|
||||
parser.add_argument(
|
||||
"-c",
|
||||
"--credentials",
|
||||
default="/etc/credentials",
|
||||
default=DEFAULT_CREDENTIALS_FILE,
|
||||
help="Path to credentials file (default: %(default)s)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--keyfile",
|
||||
default=None,
|
||||
help="Path to Fernet key file for encrypted passwords (default: auto-detect next to credentials)",
|
||||
)
|
||||
parser.add_argument("--interface", default="lanplus", choices=["lanplus", "lan"], help="IPMI interface (default: %(default)s)")
|
||||
parser.add_argument("--cipher", type=int, default=None, help="Optional IPMI cipher suite ID (e.g. 3 or 17)")
|
||||
args = parser.parse_args()
|
||||
@@ -162,7 +250,10 @@ def main():
|
||||
IPMIHOST = args.host
|
||||
IPMIINTERFACE = args.interface
|
||||
IPMICIPHER = args.cipher
|
||||
IPMIUSER, IPMIPW, IPMIEK = load_credentials(args.credentials)
|
||||
credentials_path = resolve_credentials_path(args.credentials)
|
||||
keyfile_path = resolve_keyfile(args.keyfile, credentials_path)
|
||||
fernet = load_fernet(keyfile_path) if keyfile_path else None
|
||||
IPMIUSER, IPMIPW, IPMIEK = load_credentials(credentials_path, fernet)
|
||||
|
||||
ts_print(f"{IPMIHOST}: Fan speed check for host {IPMIHOST}")
|
||||
try:
|
||||
|
||||
Reference in New Issue
Block a user