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
+155
View File
@@ -0,0 +1,155 @@
"""Certificate Authority (CA) model."""
from enum import Enum
from datetime import datetime
from gatehouse_app.extensions import db
from gatehouse_app.models.base import BaseModel
class KeyType(str, Enum):
"""SSH CA key types."""
ED25519 = "ed25519"
RSA = "rsa"
ECDSA = "ecdsa"
class CertType(str, Enum):
"""SSH certificate types."""
USER = "user"
HOST = "host"
class CA(BaseModel):
"""Certificate Authority (CA) model for SSH certificate signing.
Each organization can have multiple CAs for different purposes
(e.g., production vs. staging). Private keys are encrypted at rest
and should be protected with KMS.
"""
__tablename__ = "cas"
organization_id = db.Column(
db.String(36),
db.ForeignKey("organizations.id"),
nullable=True, # NULL for the global system-config CA
index=True,
)
# CA name and description
name = db.Column(db.String(255), nullable=False)
description = db.Column(db.Text, nullable=True)
# Key type (ED25519, RSA, ECDSA)
key_type = db.Column(
db.Enum(KeyType, values_callable=lambda x: [e.value for e in x]),
default=KeyType.ED25519,
nullable=False,
)
# Private key (encrypted at rest by database/KMS)
# Format: PEM-encoded private key
private_key = db.Column(db.Text, nullable=False)
# Public key (PEM format)
public_key = db.Column(db.Text, nullable=False)
# SHA256 fingerprint of the public key
fingerprint = db.Column(db.String(255), nullable=False, unique=True)
# CRL (Certificate Revocation List) configuration
crl_enabled = db.Column(db.Boolean, default=True, nullable=False)
crl_endpoint = db.Column(db.String(512), nullable=True)
# Default certificate validity in hours
# Can be overridden per certificate request
default_cert_validity_hours = db.Column(
db.Integer,
default=1,
nullable=False,
)
# Maximum validity duration allowed
max_cert_validity_hours = db.Column(
db.Integer,
default=24,
nullable=False,
)
# CA status
is_active = db.Column(db.Boolean, default=True, nullable=False, index=True)
# Key rotation tracking
rotated_at = db.Column(db.DateTime, nullable=True)
rotation_reason = db.Column(db.String(255), nullable=True)
# Relationships
organization = db.relationship("Organization", back_populates="cas")
certificates = db.relationship(
"SSHCertificate",
back_populates="ca",
cascade="all, delete-orphan",
)
__table_args__ = (
db.UniqueConstraint(
"organization_id", "name", name="uix_org_ca_name"
),
db.Index("idx_ca_org_active", "organization_id", "is_active"),
)
def __repr__(self):
"""String representation of CA."""
return f"<CA {self.name} (org_id={self.organization_id}, type={self.key_type})>"
def to_dict(self, exclude=None):
"""Convert CA to dictionary."""
exclude = exclude or []
# Never expose private key in API responses
exclude.extend(["private_key"])
data = super().to_dict(exclude=exclude)
# Add computed fields
data["total_certs"] = len([c for c in self.certificates if c.deleted_at is None])
data["active_certs"] = len([
c for c in self.certificates
if c.deleted_at is None and not c.revoked
])
data["revoked_certs"] = len([
c for c in self.certificates
if c.deleted_at is None and c.revoked
])
return data
def get_active_certificates(self):
"""Get all active (non-revoked) certificates issued by this CA.
Returns:
List of non-revoked SSHCertificate objects
"""
return [
c for c in self.certificates
if c.deleted_at is None and not c.revoked
]
def rotate_key(self, new_private_key, new_public_key, new_fingerprint, reason=None):
"""Rotate the CA's key pair.
This should only be done in carefully controlled circumstances.
All existing certificates remain valid but no new certs can be
signed with the old key.
Args:
new_private_key: New PEM-encoded private key
new_public_key: New PEM-encoded public key
new_fingerprint: SHA256 fingerprint of new public key
reason: Optional reason for rotation
"""
self.private_key = new_private_key
self.public_key = new_public_key
self.fingerprint = new_fingerprint
self.rotated_at = datetime.utcnow()
self.rotation_reason = reason
self.save()