Feat: Added CA-merged with Securid-Principals, Depart, Client-CLI

This commit is contained in:
2026-02-27 21:59:01 +05:45
parent 92fd57447d
commit b2212ab4d6
29 changed files with 3718 additions and 53 deletions
+115 -39
View File
@@ -2,6 +2,7 @@
import base64
from datetime import datetime
import os
import sys
import webbrowser
import requests
import argparse
@@ -22,13 +23,12 @@ import base64
load_dotenv()
# Get the API_URL from the environment variables
SIGN_URL = os.getenv("SIGN_URL", "http://localhost:1234")
SIGN_URL = os.getenv("SIGN_URL", "http://localhost:5000")
LISTENER_HOST_NAME = "127.0.0.1"
LISTENER_SERVER_PORT = 8250
CA_API_HOST = "127.0.0.1"
CA_SERVER_PORT = 1234
CACHE_FILE = 'token_cache.json' ###need to change it to secure location and permissions if used in production
CERT_FILE_PATH = "/tmp/ssl-cert"
CACHE_FILE = os.path.expanduser('~/.gatehouse/token_cache.json')
os.makedirs(os.path.dirname(CACHE_FILE), exist_ok=True)
CERT_FILE_PATH = "/tmp/ssh-cert"
CHALLENGE_FILE_PATH = "/tmp/challenge.txt"
CHALLENGE_SIG_FILE_PATH = "/tmp/challenge.txt.sig"
@@ -116,7 +116,7 @@ def decode_and_validate_token(token):
return True # Token is valid
except Exception as e:
logger.error(f"Token validation failed: {e}")
logger.debug(f"Token validation failed: {e}")
return False
def request_token():
@@ -129,19 +129,34 @@ def request_token():
logger.debug("Token loaded from cache: %s", token)
# Validate the cached token, if it exists
if token and decode_and_validate_token(token):
logger.info("Cached token is valid. Using cached token.")
return token
logger.info("No valid cached token found, proceeding to request a new token.")
token = ""
if token:
try:
if decode_and_validate_token(token):
logger.info("Cached token is valid. Using cached token.")
return token
except Exception:
pass
# Try opaque token via /auth/me
try:
r = requests.get(
f"{SIGN_URL}/api/v1/auth/me",
headers={"Authorization": f"Bearer {token}"},
timeout=5,
)
if r.status_code == 200:
logger.info("Cached session token is valid. Using cached token.")
return token
except Exception:
pass
logger.info("Cached token is expired or invalid, requesting a new token.")
token = ""
# Prepare the redirect URL for the token request
redirect_url = f"http://{LISTENER_HOST_NAME}:{LISTENER_SERVER_PORT}/?token="
logger.info("Redirect URL: %s", redirect_url)
# Construct the token request URL
token_url = f"{SIGN_URL}/token_please?redirect_url={redirect_url}"
token_url = f"{SIGN_URL}/api/v1/token_please?redirect_url={redirect_url}"
logger.info("Token request URL: %s", token_url)
# Start the web server to handle the token response
@@ -168,10 +183,10 @@ def get_activated_ssh_key():
'Authorization': f'Bearer {token}',
'Content-Type': 'application/json'
}
response = requests.get(f"{SIGN_URL}/api/ssh-keys", headers=headers)
response = requests.get(f"{SIGN_URL}/api/v1/ssh/keys", headers=headers)
if response.status_code == 200:
keys = response.json().get('ssh_keys', [])
keys = response.json().get('keys', [])
verified_keys = [key for key in keys if key['verified']]
if not verified_keys:
@@ -179,8 +194,19 @@ def get_activated_ssh_key():
exit(1)
if len(verified_keys) > 1:
logger.error("Multiple verified SSH keys found. Please specify CERT_ID.")
exit(1)
# If running interactively, let the user pick; otherwise use the most recently added key
if sys.stdout.isatty():
print("\nMultiple verified SSH keys found. Please choose one:")
for i, k in enumerate(verified_keys):
print(f" [{i+1}] {k['id'][:8]}... fingerprint={k.get('fingerprint','?')} name={k.get('key_comment','?')}")
try:
choice = int(input("Enter number: ").strip()) - 1
if 0 <= choice < len(verified_keys):
return verified_keys[choice]['id']
except (ValueError, EOFError):
pass
logger.info("Multiple verified SSH keys found; using the most recently added one.")
verified_keys.sort(key=lambda k: k.get('created_at', ''), reverse=True)
return verified_keys[0]['id']
@@ -193,26 +219,35 @@ def get_activated_ssh_key():
exit(1)
def request_certificate():
def request_certificate(principals=None):
CERT_ID = os.getenv("CERT_ID") or get_activated_ssh_key()
if not principals:
env_principals = os.getenv("PRINCIPALS")
if env_principals:
principals = [p.strip() for p in env_principals.split(',')]
else:
principals = [os.getlogin()]
headers = {
'content-type': 'application/json',
"Authorization": "bearer " + token
}
payload = {
'cert_id': CERT_ID
'cert_id': CERT_ID,
'principals': principals,
}
try:
response = requests.post(f"{SIGN_URL}/sign_cert", json=payload, headers=headers)
response = requests.post(f"{SIGN_URL}/api/v1/ssh/sign", json=payload, headers=headers)
if response.status_code == 200:
if response.status_code == 201:
json_result = response.json()
with open(CERT_FILE_PATH, 'w') as f:
f.write(json_result['certificate'])
logger.info(f"Certificate signed successfully, located at {CERT_FILE_PATH}")
logger.info(f"Valid for principals: {', '.join(json_result.get('principals', principals))}")
logger.info("You can login to your destination server with the following command")
logger.info(f"\tssh user@server -o CertificateFile={CERT_FILE_PATH}")
else:
@@ -238,14 +273,14 @@ def generate_and_sign_challenge(ssh_key_file,key_id):
# Send the POST request
response = requests.get(
f"http://{CA_API_HOST}:{CA_SERVER_PORT}/api/ssh-key/{key_id}/validationData",
f"{SIGN_URL}/api/v1/ssh/keys/{key_id}/verify",
headers=headers
)
if response.status_code!=200:
logger.error(f"Server returned unexpected code {response.status_code}")
return False
challenge_text=response.json()['validationText']+"\n"
challenge_text=response.json().get('challenge_text', response.json().get('validationText', ''))+"\n"
except Exception as e:
logger.error(f"Unable to fetch SSH Key validation data {e}")
@@ -291,7 +326,7 @@ def submit_signature_validation(signature, key_id):
# Send the POST request
response = requests.post(
f"http://{CA_API_HOST}:{CA_SERVER_PORT}/api/ssh-key/{key_id}/validate",
f"{SIGN_URL}/api/v1/ssh/keys/{key_id}/verify",
headers=headers,
json=payload
)
@@ -317,12 +352,12 @@ def remove_ssh_key(key_id=None):
}
# List keys first
response = requests.get(f"{SIGN_URL}/api/ssh-keys", headers=headers)
response = requests.get(f"{SIGN_URL}/api/v1/ssh/keys", headers=headers)
if response.status_code != 200:
logger.error(f"Failed to list SSH keys: {response.status_code} - {response.text}")
exit(1)
keys = response.json().get('ssh_keys', [])
keys = response.json().get('keys', [])
if not keys:
logger.info("No SSH keys found for your user.")
return
@@ -359,7 +394,7 @@ def remove_ssh_key(key_id=None):
exit(1)
for k in keys_to_delete:
del_response = requests.delete(f"{SIGN_URL}/api/ssh-key/{k['id']}", headers=headers)
del_response = requests.delete(f"{SIGN_URL}/api/v1/ssh/keys/{k['id']}", headers=headers)
if del_response.status_code == 200:
logger.info(f"Key {k['id']} removed successfully.")
else:
@@ -381,20 +416,40 @@ def add_ssh_key(ssh_key_file):
headers = {
'Authorization': f'Bearer {token}',
'Content-Type': 'application/json'
}
ssh_key = ssh_key_file.read().decode('utf-8')
}
if hasattr(ssh_key_file, 'read'):
# File object (e.g. argparse.FileType('rb'))
key_bytes = ssh_key_file.read()
key_path = ssh_key_file.name
elif isinstance(ssh_key_file, bytes):
key_bytes = ssh_key_file
key_path = None
else:
# String path
key_path = str(ssh_key_file)
with open(key_path, 'rb') as f:
key_bytes = f.read()
ssh_key = key_bytes.decode('utf-8').strip()
payload = {
'description': 'Added via gatehouse CLI tool',
'key': ssh_key
}
response = requests.post(f"{SIGN_URL}/api/ssh-key/add", json=payload, headers=headers)
response = requests.post(f"{SIGN_URL}/api/v1/ssh/keys", json=payload, headers=headers)
if response.status_code == 200:
ssh_key_id=response.json()['key_id']
if response.status_code == 201:
ssh_key_id=response.json()['id']
logger.info(f"SSH key {ssh_key_id} added successfully")
generate_and_sign_challenge(ssh_key_file.name,ssh_key_id)
if key_path:
# Strip .pub suffix to get the private key path for signing
private_key_path = key_path[:-4] if key_path.endswith('.pub') else key_path
generate_and_sign_challenge(private_key_path, ssh_key_id)
else:
logger.warning("No key file path available — skipping auto-verification. "
"Run with -k <path> to enable automatic key verification.")
else:
logger.error(f"Failed to add SSH key: {response.status_code} - {response.text}")
@@ -431,13 +486,15 @@ if __name__ == "__main__":
parser.add_argument("-a", "--add-key", action='store_true', default=False, help="Add SSH key to the server")
parser.add_argument("-c", "--check-cert", action='store_true', default=False, help="Check the certificate, if it's valid exit 0, if it's invalid exit 1")
parser.add_argument("-r", "--request-cert", action='store_true', default=False, help="Request that gatehouse sign a new certificate for you based on an SSH public key on file in your profile")
parser.add_argument("--principals", nargs='+', metavar='PRINCIPAL', help="Unix usernames for the certificate (default: current OS user)")
parser.add_argument("--clear-cache", action='store_true', default=False, help="Remove the cached authentication token")
parser.add_argument("--remove-key", nargs='?', const='', metavar='KEY_ID', help="Remove an SSH key from your profile. Omit KEY_ID to pick interactively.")
parser.add_argument("--list-keys", action='store_true', default=False, help="List SSH keys in your profile")
args = parser.parse_args()
# Ensure that one of --check-cert, --request-cert, or --add-key is provided
if not (args.check_cert or args.request_cert or args.add_key or args.clear_cache or args.remove_key is not None):
parser.error("At least one of --check-cert, --request-cert, --add-key, --validate-key, or --clear-cache must be provided.")
if not (args.check_cert or args.request_cert or args.add_key or args.clear_cache
or args.remove_key is not None or args.list_keys):
parser.error("At least one of --check-cert, --request-cert, --add-key, --list-keys, --remove-key, or --clear-cache must be provided.")
# Retrieve SSH key from environment variables if not provided via CLI
@@ -456,6 +513,25 @@ if __name__ == "__main__":
remove_ssh_key(args.remove_key if args.remove_key else None)
exit(0)
if args.list_keys:
request_token()
response = requests.get(
f"{SIGN_URL}/api/v1/ssh/keys",
headers={"Authorization": f"Bearer {token}"},
)
if response.status_code == 200:
data = response.json()
keys = data.get('keys', [])
if not keys:
print("No SSH keys found in your profile.")
else:
for k in keys:
verified = "✓ verified" if k.get('verified') else "✗ unverified"
print(f" {k['id']} {verified} {k.get('description', '')} (added {k['created_at'][:10]})")
else:
logger.error(f"Failed to list SSH keys: {response.status_code} - {response.text}")
exit(0)
if args.add_key:
request_token()
@@ -476,10 +552,10 @@ if __name__ == "__main__":
if args.force:
request_token()
logger.info("Forcing renewal of certificate")
request_certificate()
request_certificate(principals=args.principals)
if checkCert() == 1:
request_token()
request_certificate()
request_certificate(principals=args.principals)
exit(0)