diff --git a/instruct_kerberos.md b/instruct_kerberos.md index c8413d4..7483f46 100644 --- a/instruct_kerberos.md +++ b/instruct_kerberos.md @@ -7,7 +7,6 @@ use airflow user to perform configuration sudo -iu airflow pwd you should be located in home directory of the airflow user -image.png we now need to create keytab file which will be used to authenticate our service account @@ -19,7 +18,6 @@ have your service account ready as above commands will ask to enter service acco check that file was created and you can see content of keytab file which holds Pricipal klist -ekt airflow.keytab -image.png initiate ticket for airflow user @@ -308,4 +306,57 @@ systemctl restart "$APP_SERVICE_NAME" \ || { log_error "Failed to restart ${APP_SERVICE_NAME}"; return 1; } systemctl --no-pager status "$APP_SERVICE_NAME" -``` \ No newline at end of file +``` + +Files created +steps/05a-kerberos.sh — new deploy step (style matches 01-user-setup.sh and 06-app-service.sh). +scripts/mssql_probe.py — SQLAlchemy integrated-auth probe. +Wire it into deploy.sh +Insert between the network-mounts and app-service lines: + + +if [[ "${KRB_ENABLED:-false}" == "true" ]]; then + run_step "5a. Kerberos" "${DEPLOY_DIR}/steps/05a-kerberos.sh" +else + log_info "Skipping Kerberos setup (KRB_ENABLED=false)" +fi +Put it before 6. App Service so the ticket exists when the app starts. + +New deploy.conf variables +Var Purpose Example +KRB_ENABLED Gates the whole step true +KRB_PRINCIPAL Service principal zReconRangerDEV@PROD.ASBGROUP.CO.NZ +KRB_KEYTAB_SRC Path on the deployer where the keytab has been pre-staged (e.g. from a secret store) /root/secrets/recon-ranger.keytab +KRB_KEYTAB_PATH Where the step installs it (owned by APP_USER, mode 0600) /opt/recon-ranger/recon-ranger.keytab +KRB_CCACHE_PATH Ticket cache on disk, shared with the app service /var/lib/recon-ranger/krb5_ccache +KRB_RENEW_INTERVAL Systemd timer OnUnitActiveSec (optional, default 30min) 30min +Generate the keytab once (on a trusted host) with ktutil — same pattern as the airflow doc — then copy it to $KRB_KEYTAB_SRC before running deploy. + +Runtime env vars for the app (.env, loaded by the app service) + +KRB5CCNAME=FILE:/var/lib/recon-ranger/krb5_ccache +MSSQL_HOST=sql01.prod.example +MSSQL_PORT=1433 +MSSQL_DB=ReconRanger +MSSQL_ODBC_DRIVER=ODBC Driver 18 for SQL Server +The existing 06-app-service.sh already does EnvironmentFile=${APP_ENV_DIR}/.env, so SQLAlchemy/pyodbc will pick up KRB5CCNAME automatically — no changes to that unit needed. + +Python deps for the probe + app +Add to your app's pyproject.toml: + + +sqlalchemy +pyodbc +And on the RHEL host you'll need the MS ODBC driver (msodbcsql18) + unixODBC-devel before pyodbc can build/install. Those belong in your 03-app-install.sh or a new prereqs step. + +How the renewal works +The step installs two units: a .service (Type=oneshot, runs kinit -k -t … -c FILE:…) and a .timer that fires it every KRB_RENEW_INTERVAL. This is cleaner than airflow's Type=simple loop — systemd tracks each kinit invocation individually, so failures show up clearly in journalctl -u -kerberos-renewal.service. + +Verify after deploy: + + +systemctl list-timers | grep kerberos +sudo -u recon-ranger klist -c FILE:/var/lib/recon-ranger/krb5_ccache +sudo -u recon-ranger KRB5CCNAME=FILE:/var/lib/recon-ranger/krb5_ccache \ + /opt/recon-ranger/.venv/bin/python /opt/recon-ranger/scripts/mssql_probe.py + \ No newline at end of file diff --git a/scripts/mssql_probe.py b/scripts/mssql_probe.py new file mode 100644 index 0000000..5a9cb6b --- /dev/null +++ b/scripts/mssql_probe.py @@ -0,0 +1,54 @@ +"""Quick SQL Server connectivity probe using SQLAlchemy + Kerberos (integrated auth). + +Prereqs on the host: + - krb5-workstation installed and /etc/krb5.conf configured + - A valid TGT in the cache pointed to by $KRB5CCNAME + e.g. KRB5CCNAME=FILE:/var/lib/recon-ranger/krb5_ccache + - Microsoft ODBC Driver 18 for SQL Server (msodbcsql18) + unixODBC + - Python packages: sqlalchemy, pyodbc + +Usage: + MSSQL_HOST=sql01.prod.example MSSQL_DB=ReconRanger \ + python scripts/mssql_probe.py +""" + +from __future__ import annotations + +import os +import sys +from urllib.parse import quote_plus + +from sqlalchemy import create_engine, text + + +def build_url() -> str: + host = os.environ["MSSQL_HOST"] + database = os.environ["MSSQL_DB"] + port = os.environ.get("MSSQL_PORT", "1433") + driver = os.environ.get("MSSQL_ODBC_DRIVER", "ODBC Driver 18 for SQL Server") + + odbc = ( + f"DRIVER={{{driver}}};" + f"SERVER={host},{port};" + f"DATABASE={database};" + "Trusted_Connection=yes;" + "Encrypt=yes;" + "TrustServerCertificate=yes;" + ) + return f"mssql+pyodbc:///?odbc_connect={quote_plus(odbc)}" + + +def main() -> int: + engine = create_engine(build_url(), pool_pre_ping=True) + with engine.connect() as conn: + row = conn.execute( + text("SELECT SUSER_SNAME() AS login, DB_NAME() AS db, @@VERSION AS version") + ).one() + print(f"Logged in as : {row.login}") + print(f"Database : {row.db}") + print(f"Server : {row.version.splitlines()[0]}") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/steps/05a-kerberos.sh b/steps/05a-kerberos.sh new file mode 100644 index 0000000..b38061e --- /dev/null +++ b/steps/05a-kerberos.sh @@ -0,0 +1,96 @@ +#!/usr/bin/env bash +# ============================================================================= +# 05a-kerberos.sh — Install Kerberos client, stage keytab, and install a +# systemd timer that renews the TGT on a schedule. +# +# The app service reads KRB5CCNAME from its .env file and uses the shared +# ticket cache when opening SQL Server connections via pyodbc/SQLAlchemy. +# ============================================================================= + +require_vars APP_USER APP_GROUP \ + KRB_PRINCIPAL KRB_KEYTAB_SRC KRB_KEYTAB_PATH KRB_CCACHE_PATH \ + || return 1 + +KRB_RENEW_INTERVAL="${KRB_RENEW_INTERVAL:-30min}" +KRB_RENEWAL_SERVICE="${APP_SERVICE_NAME:-recon-ranger}-kerberos-renewal" + +# ---- Install the Kerberos client ------------------------------------------ +if ! command -v kinit &>/dev/null; then + log_info "Installing krb5-workstation" + dnf install -y krb5-workstation \ + || { log_error "Failed to install krb5-workstation"; return 1; } +else + log_info "krb5-workstation already installed" +fi + +# ---- Sanity check /etc/krb5.conf ------------------------------------------ +if [[ ! -s /etc/krb5.conf ]] || ! grep -q "default_realm" /etc/krb5.conf; then + log_error "/etc/krb5.conf missing or has no default_realm — configure the realm first" + return 1 +fi + +# ---- Stage the keytab at the target path ---------------------------------- +if [[ ! -f "$KRB_KEYTAB_SRC" ]]; then + log_error "Keytab source not found: $KRB_KEYTAB_SRC" + return 1 +fi + +install -d -o "$APP_USER" -g "$APP_GROUP" -m 0750 "$(dirname "$KRB_KEYTAB_PATH")" +install -o "$APP_USER" -g "$APP_GROUP" -m 0600 "$KRB_KEYTAB_SRC" "$KRB_KEYTAB_PATH" + +# Show what is inside the keytab so deploy logs record the principals/enctypes. +log_info "Keytab contents:" +klist -ekt "$KRB_KEYTAB_PATH" || log_warn "Could not list keytab contents" + +# ---- Prepare the ticket cache directory ----------------------------------- +install -d -o "$APP_USER" -g "$APP_GROUP" -m 0700 "$(dirname "$KRB_CCACHE_PATH")" + +# ---- Prime the cache immediately ------------------------------------------ +log_info "Running initial kinit as $APP_USER" +sudo -u "$APP_USER" \ + kinit -k -t "$KRB_KEYTAB_PATH" -c "FILE:$KRB_CCACHE_PATH" "$KRB_PRINCIPAL" \ + || { log_error "Initial kinit failed for $KRB_PRINCIPAL"; return 1; } + +sudo -u "$APP_USER" klist -c "FILE:$KRB_CCACHE_PATH" \ + || log_warn "klist failed after kinit" + +# ---- Renewal service (oneshot, fires kinit) ------------------------------- +RENEWAL_UNIT="/etc/systemd/system/${KRB_RENEWAL_SERVICE}.service" +RENEWAL_TIMER="/etc/systemd/system/${KRB_RENEWAL_SERVICE}.timer" + +log_info "Writing $RENEWAL_UNIT" +cat > "$RENEWAL_UNIT" < "$RENEWAL_TIMER" <