This commit is contained in:
root
2025-11-25 10:11:32 +03:00
parent 48b1934def
commit 60792735ad
38 changed files with 12695 additions and 0 deletions

View File

@ -0,0 +1,239 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
SQL generator for creating Guacamole user with custom password
Uses same hashing algorithm as Guacamole:
- SHA-256(password_bytes + salt_bytes)
- Random 32-byte salt
Usage:
python generate_guacamole_user.py --username admin --password MySecurePass123
python generate_guacamole_user.py --username admin --password MySecurePass123 --admin
"""
import hashlib
import secrets
import argparse
import sys
import io
# Fix Windows encoding issues
if sys.platform == 'win32':
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8')
def generate_guacamole_password_hash(password: str) -> tuple[bytes, bytes]:
"""
Generate hash and salt for Guacamole password
CORRECT ALGORITHM (verified 2025-10-29):
Guacamole uses: SHA-256(password_string + salt_hex_string)
IMPORTANT: Salt converted to HEX string BEFORE hashing!
Args:
password: Password in plain text
Returns:
Tuple (password_hash, password_salt) as bytes for PostgreSQL
"""
# Generate random 32-byte salt
salt_bytes = secrets.token_bytes(32)
# CRITICAL: Convert salt to HEX STRING (uppercase)
# Guacamole hashes: password + hex(salt), NOT password + binary(salt)!
salt_hex_string = salt_bytes.hex().upper()
# Compute SHA-256(password_string + salt_hex_string)
# Concatenate password STRING + salt HEX STRING, then encode to UTF-8
hash_input = password + salt_hex_string
password_hash = hashlib.sha256(hash_input.encode('utf-8')).digest()
return password_hash, salt_bytes
def bytes_to_postgres_hex(data: bytes) -> str:
"""
Convert bytes to PostgreSQL hex format for decode()
Args:
data: Bytes to convert
Returns:
String in 'HEXSTRING' format for use in decode('...', 'hex')
"""
return data.hex().upper()
def generate_sql(username: str, password: str, is_admin: bool = False) -> str:
"""
Generate SQL for creating Guacamole user
Args:
username: Username
password: Password
is_admin: If True, grant full administrator privileges
Returns:
SQL script to execute
"""
password_hash, password_salt = generate_guacamole_password_hash(password)
hash_hex = bytes_to_postgres_hex(password_hash)
salt_hex = bytes_to_postgres_hex(password_salt)
sql = f"""-- Generated Guacamole user creation SQL
-- Username: {username}
-- Password: {'*' * len(password)} (length: {len(password)})
-- Generated with: generate_guacamole_user.py
-- Create user entity
INSERT INTO guacamole_entity (name, type)
VALUES ('{username}', 'USER');
-- Create user with password hash
INSERT INTO guacamole_user (entity_id, password_hash, password_salt, password_date)
SELECT
entity_id,
decode('{hash_hex}', 'hex'),
decode('{salt_hex}', 'hex'),
CURRENT_TIMESTAMP
FROM guacamole_entity
WHERE name = '{username}' AND guacamole_entity.type = 'USER';
"""
if is_admin:
sql += f"""
-- Grant all system permissions (administrator)
INSERT INTO guacamole_system_permission (entity_id, permission)
SELECT entity_id, permission::guacamole_system_permission_type
FROM (
VALUES
('{username}', 'CREATE_CONNECTION'),
('{username}', 'CREATE_CONNECTION_GROUP'),
('{username}', 'CREATE_SHARING_PROFILE'),
('{username}', 'CREATE_USER'),
('{username}', 'CREATE_USER_GROUP'),
('{username}', 'ADMINISTER')
) permissions (username, permission)
JOIN guacamole_entity ON permissions.username = guacamole_entity.name AND guacamole_entity.type = 'USER';
-- Grant permission to read/update/administer self
INSERT INTO guacamole_user_permission (entity_id, affected_user_id, permission)
SELECT guacamole_entity.entity_id, guacamole_user.user_id, permission::guacamole_object_permission_type
FROM (
VALUES
('{username}', '{username}', 'READ'),
('{username}', '{username}', 'UPDATE'),
('{username}', '{username}', 'ADMINISTER')
) permissions (username, affected_username, permission)
JOIN guacamole_entity ON permissions.username = guacamole_entity.name AND guacamole_entity.type = 'USER'
JOIN guacamole_entity affected ON permissions.affected_username = affected.name AND guacamole_entity.type = 'USER'
JOIN guacamole_user ON guacamole_user.entity_id = affected.entity_id;
"""
return sql
def main():
parser = argparse.ArgumentParser(
description='Generate SQL for creating Guacamole user with custom password',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
# Create regular user
python generate_guacamole_user.py --username john --password SecurePass123
# Create administrator user
python generate_guacamole_user.py --username admin --password AdminPass456 --admin
# Save to file
python generate_guacamole_user.py --username admin --password AdminPass456 --admin > 002-custom-admin.sql
# Apply directly to running database
python generate_guacamole_user.py --username admin --password AdminPass456 --admin | \\
docker compose exec -T postgres psql -U guacamole_user -d guacamole_db
SECURITY NOTES:
- Never commit generated SQL files with passwords to git!
- Use strong passwords (minimum 16 characters, mixed case, numbers, symbols)
- Change default passwords immediately after deployment
- Store passwords securely (password manager, secrets vault)
"""
)
parser.add_argument(
'--username',
required=True,
help='Username for the new Guacamole user'
)
parser.add_argument(
'--password',
required=True,
help='Password for the new user (plain text)'
)
parser.add_argument(
'--admin',
action='store_true',
help='Grant administrator privileges (ADMINISTER system permission)'
)
parser.add_argument(
'--verify',
action='store_true',
help='Verify password by generating hash twice'
)
args = parser.parse_args()
# Validate password strength
if len(args.password) < 8:
print("[WARNING] Password is too short (< 8 characters)", file=sys.stderr)
print(" Recommended: minimum 16 characters with mixed case, numbers, symbols", file=sys.stderr)
response = input("Continue anyway? (y/N): ")
if response.lower() != 'y':
sys.exit(1)
# Verify if requested
if args.verify:
print("[VERIFY] Verifying hash generation...", file=sys.stderr)
hash1, salt1 = generate_guacamole_password_hash(args.password)
hash2, salt2 = generate_guacamole_password_hash(args.password)
# Salts should be different (random)
if salt1 == salt2:
print("[ERROR] Salt generation not random!", file=sys.stderr)
sys.exit(1)
# But if we use same salt, hash should be same
# Use correct algorithm: SHA256(password_string + salt_hex_string)
salt_hex_string = salt1.hex().upper()
hash_test = hashlib.sha256((args.password + salt_hex_string).encode('utf-8')).digest()
if hash_test == hash1:
print("[OK] Hash generation verified", file=sys.stderr)
else:
print("[ERROR] Hash generation mismatch!", file=sys.stderr)
sys.exit(1)
# Generate SQL
sql = generate_sql(args.username, args.password, args.admin)
# Output
print(sql)
# Print info to stderr (so it doesn't interfere with piping SQL)
role = "Administrator" if args.admin else "Regular User"
print(f"\n[OK] SQL generated successfully!", file=sys.stderr)
print(f" Username: {args.username}", file=sys.stderr)
print(f" Role: {role}", file=sys.stderr)
print(f" Password length: {len(args.password)} characters", file=sys.stderr)
print(f"\n[INFO] To apply this SQL:", file=sys.stderr)
print(f" docker compose exec -T postgres psql -U guacamole_user -d guacamole_db < output.sql", file=sys.stderr)
if __name__ == '__main__':
main()