mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-08 03:38:23 +00:00
Deploy GitLab from the official CNG CE images as six services: webservice (puma) and sidekiq scale to the topology default, workhorse is the sole HTTP entry, gitaly and a one-shot migrations job are pinned singletons, shell runs gitlab-sshd. The Omnibus container, its Dockerfile and the GITLAB_OMNIBUS_* config groups are removed. All rails/workhorse/shell/gitaly configs and shared secrets are pre-rendered by Ansible and mounted read-only; eleven credentials land in meta/schema.yml including an rsa_pem_2048 OIDC signing key (new value_generator algorithm). Postgres, redis and S3 object storage ride the existing lookups; OIDC, SMTP, the public SSH port and the root password are preserved. compose_volumes gains a per-volume nfs:false opt-out so the gitaly repositories volume stays node-local in swarm (upstream forbids git repositories on NFS); nfs_prep skips volumes without driver_opts accordingly. The Nextcloud gitlab integration execs the CNG rails runner. ruby_single_line dies with its only consumer; ruby_dq keeps test coverage. Ports (8181/8080/2222/8075/3808/9122) and service names resolve from single SPOT declarations in meta/services.yml via vars constants; the version pin sits on the webservice image pair so the updater can bump it. Live-validated in compose up to all six services healthy and migrations completed; fresh-install-only semantics for existing Omnibus data are documented. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
120 lines
4.4 KiB
Python
120 lines
4.4 KiB
Python
import base64
|
|
import hashlib
|
|
import re
|
|
import secrets
|
|
import string
|
|
|
|
import bcrypt
|
|
from cryptography.hazmat.primitives.asymmetric import ec, rsa
|
|
from cryptography.hazmat.primitives.serialization import (
|
|
Encoding,
|
|
NoEncryption,
|
|
PrivateFormat,
|
|
PublicFormat,
|
|
)
|
|
|
|
|
|
class ValueGenerator:
|
|
PASSWORD_REGEX = re.compile(
|
|
r"^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[^A-Za-z0-9]).{12,}$"
|
|
)
|
|
|
|
def __init__(self) -> None:
|
|
self._vapid_keypair: tuple[str, str] | None = None
|
|
|
|
def generate_strong_password(self, length: int = 32) -> str:
|
|
if length < 12:
|
|
raise ValueError("Password length must be at least 12 characters")
|
|
|
|
characters = string.ascii_letters + string.digits + "!@#$%^&*()-_=+[]:,.?"
|
|
|
|
for _ in range(10_000):
|
|
password = "".join(secrets.choice(characters) for _ in range(length))
|
|
if self._is_valid_password(password):
|
|
return password
|
|
|
|
raise RuntimeError("Failed to generate a valid password after many attempts")
|
|
|
|
def _is_valid_password(self, password: str) -> bool:
|
|
return bool(self.PASSWORD_REGEX.match(password))
|
|
|
|
def generate_secure_alphanumeric(self, length: int) -> str:
|
|
"""Generate a cryptographically secure random alphanumeric string of the given length."""
|
|
characters = string.ascii_letters + string.digits
|
|
return "".join(secrets.choice(characters) for _ in range(length))
|
|
|
|
def generate_vapid_keypair(self) -> tuple[str, str]:
|
|
"""Return a linked (private, public) VAPID keypair, cached per instance.
|
|
|
|
Returns:
|
|
A tuple of unpadded base64url strings: the P-256 private scalar
|
|
(32 bytes) and the uncompressed public point (65 bytes, 0x04||X||Y),
|
|
in the format the Web Push / mastodon ecosystem expects.
|
|
"""
|
|
if self._vapid_keypair is None:
|
|
private_key = ec.generate_private_key(ec.SECP256R1())
|
|
scalar = private_key.private_numbers().private_value.to_bytes(32, "big")
|
|
point = private_key.public_key().public_bytes(
|
|
Encoding.X962, PublicFormat.UncompressedPoint
|
|
)
|
|
self._vapid_keypair = (
|
|
base64.urlsafe_b64encode(scalar).rstrip(b"=").decode(),
|
|
base64.urlsafe_b64encode(point).rstrip(b"=").decode(),
|
|
)
|
|
return self._vapid_keypair
|
|
|
|
def generate_value(self, algorithm: str) -> str:
|
|
"""
|
|
Generate a random secret value according to the specified algorithm.
|
|
|
|
Supported algorithms:
|
|
• "random_hex"
|
|
• "random_hex_32"
|
|
• "random_hex_16"
|
|
• "sha256"
|
|
• "sha1"
|
|
• "strong_password"
|
|
• "bcrypt"
|
|
• "alphanumeric"
|
|
• "base64_prefixed_32"
|
|
• "vapid_private"
|
|
• "vapid_public"
|
|
• "rsa_pem_2048"
|
|
"""
|
|
if algorithm == "random_hex":
|
|
return secrets.token_hex(64)
|
|
if algorithm == "random_hex_32":
|
|
return secrets.token_hex(32)
|
|
if algorithm == "random_hex_16":
|
|
return secrets.token_hex(16)
|
|
if algorithm == "sha256":
|
|
return hashlib.sha256(secrets.token_bytes(32)).hexdigest()
|
|
if algorithm == "sha1":
|
|
return hashlib.sha1(
|
|
secrets.token_bytes(20),
|
|
usedforsecurity=False,
|
|
).hexdigest()
|
|
if algorithm == "strong_password":
|
|
return self.generate_strong_password(32)
|
|
if algorithm == "bcrypt":
|
|
pw = secrets.token_urlsafe(16).encode()
|
|
raw_hash = bcrypt.hashpw(pw, bcrypt.gensalt()).decode()
|
|
alnum = string.digits + string.ascii_lowercase
|
|
return "".join(
|
|
secrets.choice(alnum) if ch == "$" else ch for ch in raw_hash
|
|
)
|
|
if algorithm == "alphanumeric":
|
|
return self.generate_secure_alphanumeric(64)
|
|
if algorithm == "base64_prefixed_32":
|
|
return "base64:" + base64.b64encode(secrets.token_bytes(32)).decode()
|
|
if algorithm == "vapid_private":
|
|
return self.generate_vapid_keypair()[0]
|
|
if algorithm == "vapid_public":
|
|
return self.generate_vapid_keypair()[1]
|
|
if algorithm == "rsa_pem_2048":
|
|
key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
|
|
return key.private_bytes(
|
|
Encoding.PEM, PrivateFormat.TraditionalOpenSSL, NoEncryption()
|
|
).decode()
|
|
return "undefined"
|