#!/usr/bin/env bash set +x set -Eeuo pipefail umask 077 readonly EXPECTED_ACCOUNT="billy@livingip.xyz" readonly PROJECT_ID="teleo-501523" readonly INSTANCE="teleo-prod-1" readonly ZONE="europe-west6-a" readonly EXPECTED_INTERNAL_IP="10.60.0.3" readonly KEYCHAIN_SERVICE="com.livingip.teleo.gcp-operator-reauth" readonly KEYCHAIN_LABEL="Teleo GCP operator emergency password" readonly WORKFLOW_PATH=".github/workflows/gcp-iap-operator.yml" readonly RERUN_COMMAND="scripts/gcp-operator-reauth.sh --status" HELPER_DIR="" KEYCHAIN_HELPER="" usage() { cat <<'EOF' Usage: scripts/gcp-operator-reauth.sh [--status | --store-password] --status Report Keychain presence and local gcloud/IAP prerequisites. --store-password Securely store or update the emergency Google password, then report status. With no option, the script asks before opening the secure password prompt. EOF } die() { printf 'gcp-operator-reauth: %s\n' "$1" >&2 exit 2 } cleanup() { if [[ -n "$HELPER_DIR" && -d "$HELPER_DIR" ]]; then rm -rf -- "$HELPER_DIR" fi } trap cleanup EXIT validate_test_overrides() { if [[ -n "${TELEO_GCP_REAUTH_KEYCHAIN_HELPER:-}${TELEO_GCP_REAUTH_PINENTRY:-}" && \ "${TELEO_GCP_REAUTH_TESTING:-0}" != "1" ]]; then die "helper overrides are accepted only when TELEO_GCP_REAUTH_TESTING=1" fi } build_keychain_helper() { if [[ -n "${TELEO_GCP_REAUTH_KEYCHAIN_HELPER:-}" ]]; then [[ -x "$TELEO_GCP_REAUTH_KEYCHAIN_HELPER" ]] || die "test Keychain helper is not executable" KEYCHAIN_HELPER="$TELEO_GCP_REAUTH_KEYCHAIN_HELPER" return fi command -v swiftc >/dev/null 2>&1 || die "swiftc is required for secure macOS Keychain access" HELPER_DIR="$(mktemp -d "${TMPDIR:-/tmp}/teleo-gcp-keychain.XXXXXX")" chmod 0700 "$HELPER_DIR" local source="$HELPER_DIR/GcpOperatorKeychain.swift" KEYCHAIN_HELPER="$HELPER_DIR/gcp-operator-keychain" cat >"$source" <<'SWIFT' import AppKit import Foundation import Security func fail(_ message: String, _ code: Int32 = 2) -> Never { FileHandle.standardError.write(Data("gcp-operator-keychain: \(message)\n".utf8)) exit(code) } guard CommandLine.arguments.count == 5 else { fail("expected action, service, account, and label") } let action = CommandLine.arguments[1] let service = CommandLine.arguments[2] let account = CommandLine.arguments[3] let label = CommandLine.arguments[4] func itemQuery() -> [String: Any] { [ kSecClass as String: kSecClassGenericPassword, kSecAttrService as String: service, kSecAttrAccount as String: account, kSecAttrSynchronizable as String: kCFBooleanFalse as Any, ] } func itemExists() -> Bool { var query = itemQuery() query[kSecMatchLimit as String] = kSecMatchLimitOne let status = SecItemCopyMatching(query as CFDictionary, nil) if status == errSecSuccess { return true } if status == errSecItemNotFound { return false } fail("Keychain status failed with OSStatus \(status)") } func store(_ password: Data) -> String { guard !password.isEmpty else { fail("empty password was not stored") } guard password.count <= 4096 else { fail("password exceeds the 4096-byte limit") } let query = itemQuery() if itemExists() { let attributes: [String: Any] = [ kSecValueData as String: password, kSecAttrLabel as String: label, kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlockedThisDeviceOnly, ] let status = SecItemUpdate(query as CFDictionary, attributes as CFDictionary) guard status == errSecSuccess else { fail("Keychain update failed with OSStatus \(status)") } return "updated" } var item = query item[kSecValueData as String] = password item[kSecAttrLabel as String] = label item[kSecAttrAccessible as String] = kSecAttrAccessibleWhenUnlockedThisDeviceOnly let status = SecItemAdd(item as CFDictionary, nil) guard status == errSecSuccess else { fail("Keychain add failed with OSStatus \(status)") } return "stored" } func hexValue(_ byte: UInt8) -> UInt8? { switch byte { case 48...57: return byte - 48 case 65...70: return byte - 55 case 97...102: return byte - 87 default: return nil } } func decodeAssuan(_ value: Substring) -> Data? { let input = Array(value.utf8) var output = Data() var index = 0 while index < input.count { if input[index] == 37 { guard index + 2 < input.count, let high = hexValue(input[index + 1]), let low = hexValue(input[index + 2]) else { return nil } output.append((high << 4) | low) index += 3 } else { output.append(input[index]) index += 1 } } return output } func passwordFromPinentry(_ response: Data) -> Data { guard let text = String(data: response, encoding: .utf8) else { fail("pinentry returned non-UTF-8 protocol data") } for line in text.split(separator: "\n", omittingEmptySubsequences: false) { if line.hasPrefix("D ") { guard let decoded = decodeAssuan(line.dropFirst(2)) else { fail("pinentry returned invalid escaped data") } return decoded } } fail("pinentry did not return a password") } switch action { case "status": print(itemExists() ? "present" : "absent") case "store-pinentry": let response = FileHandle.standardInput.readDataToEndOfFile() print(store(passwordFromPinentry(response))) case "prompt-store": let app = NSApplication.shared app.setActivationPolicy(.accessory) app.activate(ignoringOtherApps: true) let alert = NSAlert() alert.alertStyle = .informational alert.messageText = "Store Teleo GCP emergency password" alert.informativeText = "This password is stored only in this Mac's encrypted Keychain. It cannot refresh gcloud OAuth by itself." alert.addButton(withTitle: "Store in Keychain") alert.addButton(withTitle: "Cancel") let field = NSSecureTextField(frame: NSRect(x: 0, y: 0, width: 360, height: 24)) field.placeholderString = "Current Google password" let mainMenu = NSMenu() let editMenuItem = NSMenuItem(title: "Edit", action: nil, keyEquivalent: "") let editMenu = NSMenu(title: "Edit") editMenu.addItem(withTitle: "Paste", action: #selector(NSText.paste(_:)), keyEquivalent: "v") editMenuItem.submenu = editMenu mainMenu.addItem(editMenuItem) app.mainMenu = mainMenu let fieldMenu = NSMenu(title: "Edit") fieldMenu.addItem(withTitle: "Paste", action: #selector(NSText.paste(_:)), keyEquivalent: "") field.menu = fieldMenu alert.accessoryView = field alert.window.initialFirstResponder = field while true { app.activate(ignoringOtherApps: true) guard alert.runModal() == .alertFirstButtonReturn else { fail("password entry cancelled", 3) } if !field.stringValue.isEmpty { print(store(Data(field.stringValue.utf8))) break } NSSound.beep() alert.informativeText = "Enter the current Google password. Empty values are never stored." alert.window.initialFirstResponder = field } default: fail("unsupported action") } SWIFT swiftc -O -framework AppKit -framework Security "$source" -o "$KEYCHAIN_HELPER" chmod 0700 "$KEYCHAIN_HELPER" } find_pinentry() { if [[ -n "${TELEO_GCP_REAUTH_PINENTRY:-}" ]]; then [[ -x "$TELEO_GCP_REAUTH_PINENTRY" ]] || die "test pinentry helper is not executable" printf '%s\n' "$TELEO_GCP_REAUTH_PINENTRY" return fi if command -v pinentry-mac >/dev/null 2>&1; then command -v pinentry-mac return fi for candidate in /opt/homebrew/bin/pinentry-mac /usr/local/bin/pinentry-mac; do if [[ -x "$candidate" ]]; then printf '%s\n' "$candidate" return fi done return 1 } store_password() { local pinentry result if pinentry="$(find_pinentry)"; then result="$({ printf 'SETTITLE Teleo GCP operator\n' printf 'SETDESC Store the current Google password in macOS Keychain. This does not authenticate gcloud.\n' printf 'SETPROMPT Google password:\n' printf 'GETPIN\n' printf 'BYE\n' } | "$pinentry" | "$KEYCHAIN_HELPER" store-pinentry "$KEYCHAIN_SERVICE" "$EXPECTED_ACCOUNT" "$KEYCHAIN_LABEL")" || { die "secure password entry was cancelled or failed" } printf 'secure_prompt=pinentry-mac\n' else result="$("$KEYCHAIN_HELPER" prompt-store "$KEYCHAIN_SERVICE" "$EXPECTED_ACCOUNT" "$KEYCHAIN_LABEL")" || { die "secure password entry was cancelled or failed" } printf 'secure_prompt=AppKit NSSecureTextField\n' fi case "$result" in stored | updated) printf 'keychain_password=%s\n' "$result" ;; *) die "Keychain helper returned an invalid result" ;; esac } emit_security_boundary() { printf '%s\n' \ 'security_truth=The stored Google password is emergency convenience only; it cannot satisfy or automate gcloud OAuth.' \ 'password_handling=The script never prints, copies, or submits the password and never places it in a command argument.' \ "durable_unattended_route=$WORKFLOW_PATH uses GitHub OIDC/Workload Identity Federation and IAP after one-time bootstrap." } emit_reauth_cta() { printf '%s\n' \ 'oauth_state=stale_or_unavailable' \ "clear_CTA=Run 'gcloud auth login $EXPECTED_ACCOUNT --force --no-launch-browser' in a terminal, paste the displayed URL into the dedicated Chrome GCP session, complete reauthentication, return the authorization code to gcloud, then rerun '$RERUN_COMMAND'." } report_status() { local keychain_state account project instance_state gcloud_bin keychain_state="$("$KEYCHAIN_HELPER" status "$KEYCHAIN_SERVICE" "$EXPECTED_ACCOUNT" "$KEYCHAIN_LABEL")" case "$keychain_state" in present | absent) ;; *) die "Keychain helper returned an invalid status" ;; esac emit_security_boundary printf 'keychain_password=%s\n' "$keychain_state" if ! gcloud_bin="$(command -v gcloud)"; then printf '%s\n' 'gcloud_state=missing' 'operator_readiness=not_ready' return 1 fi printf 'gcloud_binary=%s\n' "$gcloud_bin" account="$(gcloud config get-value account --quiet 2>/dev/null || true)" project="$(gcloud config get-value project --quiet 2>/dev/null || true)" printf 'gcloud_account=%s\n' "${account:-unset}" printf 'gcloud_project=%s\n' "${project:-unset}" if [[ "$account" != "$EXPECTED_ACCOUNT" || "$project" != "$PROJECT_ID" ]]; then printf '%s\n' \ 'gcloud_config_state=mismatch' \ "clear_CTA=Run 'gcloud config set account $EXPECTED_ACCOUNT && gcloud config set project $PROJECT_ID', then rerun '$RERUN_COMMAND'." \ 'operator_readiness=not_ready' return 1 fi printf 'gcloud_config_state=ready\n' if ! gcloud auth print-access-token --account="$EXPECTED_ACCOUNT" >/dev/null 2>&1; then emit_reauth_cta printf 'operator_readiness=not_ready\n' return 1 fi printf 'oauth_state=ready\n' if ! command -v ssh >/dev/null 2>&1; then printf '%s\n' 'ssh_binary=missing' 'operator_readiness=not_ready' return 1 fi printf 'ssh_binary=ready\n' instance_state="$( gcloud compute instances describe "$INSTANCE" \ --project="$PROJECT_ID" \ --zone="$ZONE" \ --format='value(name,status,networkInterfaces[0].networkIP)' \ --quiet 2>/dev/null || true )" if [[ "$instance_state" != "$INSTANCE"$'\t'"RUNNING"$'\t'"$EXPECTED_INTERNAL_IP" ]]; then printf '%s\n' \ 'instance_readback=not_ready' \ "clear_CTA=Confirm $INSTANCE is RUNNING at $EXPECTED_INTERNAL_IP in $PROJECT_ID/$ZONE, then rerun '$RERUN_COMMAND'." \ 'operator_readiness=not_ready' return 1 fi printf 'instance_readback=%s RUNNING %s\n' "$INSTANCE" "$EXPECTED_INTERNAL_IP" if ! gcloud compute ssh "$INSTANCE" \ --project="$PROJECT_ID" \ --zone="$ZONE" \ --tunnel-through-iap \ --ssh-key-expire-after=5m \ --dry-run \ --quiet >/dev/null 2>&1; then printf '%s\n' \ 'iap_ssh_preflight=not_ready' \ "clear_CTA=Repair the local gcloud IAP/SSH prerequisite reported by 'gcloud compute ssh $INSTANCE --project=$PROJECT_ID --zone=$ZONE --tunnel-through-iap --ssh-key-expire-after=5m --dry-run', then rerun '$RERUN_COMMAND'." \ 'operator_readiness=not_ready' return 1 fi printf '%s\n' \ 'iap_ssh_preflight=ready_dry_run_only' \ 'iap_connection=not_attempted' \ 'operator_readiness=ready_for_explicit_operator_action' } main() { local mode="interactive" answer [[ $# -le 1 ]] || { usage >&2 exit 2 } if [[ $# -eq 1 ]]; then case "$1" in --status) mode="status" ;; --store-password) mode="store" ;; -h | --help) usage; exit 0 ;; *) usage >&2; exit 2 ;; esac fi validate_test_overrides build_keychain_helper if [[ "$mode" == "interactive" ]]; then if [[ ! -t 0 && "${TELEO_GCP_REAUTH_TESTING:-0}" != "1" ]]; then die "interactive mode requires a terminal; use --status or --store-password" fi printf 'Store or update the emergency Google password in macOS Keychain? [y/N] ' >&2 IFS= read -r answer || answer="" case "$answer" in y | Y | yes | YES) store_password ;; *) printf 'keychain_password=unchanged\n' ;; esac elif [[ "$mode" == "store" ]]; then store_password fi report_status } main "$@"