Changed ephemeral key to 30m lifespan; keys stored in /dev/shm; explicit 0600 perms; delete keys when session opens.

This commit is contained in:
2026-02-03 09:17:15 +00:00
parent 667b02f0c3
commit f54cc3f09b
3 changed files with 15 additions and 2 deletions

View File

@@ -83,6 +83,7 @@ def _sign_public_key(
serial: int, serial: int,
validity_days: int, validity_days: int,
comment: str, comment: str,
validity_override: str | None = None,
) -> str: ) -> str:
if not ca_private_key or not ca_public_key: if not ca_private_key or not ca_public_key:
raise RuntimeError("CA material missing") raise RuntimeError("CA material missing")
@@ -102,7 +103,7 @@ def _sign_public_key(
"-n", "-n",
principal, principal,
"-V", "-V",
f"+{validity_days}d", validity_override or f"+{validity_days}d",
"-z", "-z",
str(serial), str(serial),
pubkey_path, pubkey_path,

View File

@@ -8,6 +8,7 @@ import tempfile
from channels.db import database_sync_to_async from channels.db import database_sync_to_async
from channels.generic.websocket import AsyncWebsocketConsumer from channels.generic.websocket import AsyncWebsocketConsumer
from django.conf import settings
from django.utils import timezone from django.utils import timezone
from apps.audit.matching import find_matching_event_type from apps.audit.matching import find_matching_event_type
@@ -108,7 +109,8 @@ class ShellConsumer(AsyncWebsocketConsumer):
async def _start_ssh(self, user): async def _start_ssh(self, user):
# Generate a short-lived keypair + SSH certificate and then # Generate a short-lived keypair + SSH certificate and then
# bridge the WebSocket to an SSH subprocess. # bridge the WebSocket to an SSH subprocess.
self.tempdir = tempfile.TemporaryDirectory(prefix="keywarden-shell-") temp_base = "/dev/shm" if os.path.isdir("/dev/shm") and os.access("/dev/shm", os.W_OK) else None
self.tempdir = tempfile.TemporaryDirectory(prefix="keywarden-shell-", dir=temp_base)
key_path, cert_path = await asyncio.to_thread( key_path, cert_path = await asyncio.to_thread(
_generate_session_keypair, _generate_session_keypair,
self.tempdir.name, self.tempdir.name,
@@ -152,6 +154,13 @@ class ShellConsumer(AsyncWebsocketConsumer):
stdout=asyncio.subprocess.PIPE, stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.STDOUT, stderr=asyncio.subprocess.STDOUT,
) )
for path in (key_path, cert_path, f"{key_path}.pub"):
try:
os.remove(path)
except FileNotFoundError:
continue
except Exception:
pass
self.reader_task = asyncio.create_task(self._stream_output()) self.reader_task = asyncio.create_task(self._stream_output())
async def _stream_output(self): async def _stream_output(self):
@@ -246,6 +255,7 @@ def _generate_session_keypair(tempdir: str, user, principal: str) -> tuple[str,
raise RuntimeError("ssh-keygen not available") from exc raise RuntimeError("ssh-keygen not available") from exc
except subprocess.CalledProcessError as exc: except subprocess.CalledProcessError as exc:
raise RuntimeError(f"ssh-keygen failed: {exc.stderr.decode('utf-8', 'ignore')}") from exc raise RuntimeError(f"ssh-keygen failed: {exc.stderr.decode('utf-8', 'ignore')}") from exc
os.chmod(key_path, 0o600)
pubkey_path = key_path + ".pub" pubkey_path = key_path + ".pub"
with open(pubkey_path, "r", encoding="utf-8") as handle: with open(pubkey_path, "r", encoding="utf-8") as handle:
public_key = handle.read().strip() public_key = handle.read().strip()
@@ -257,6 +267,7 @@ def _generate_session_keypair(tempdir: str, user, principal: str) -> tuple[str,
principal=principal, principal=principal,
serial=serial, serial=serial,
validity_days=1, validity_days=1,
validity_override=f"+{settings.KEYWARDEN_SHELL_CERT_VALIDITY_MINUTES}m",
comment=identity, comment=identity,
) )
cert_path = key_path + "-cert.pub" cert_path = key_path + "-cert.pub"

View File

@@ -103,6 +103,7 @@ SESSION_CACHE_ALIAS = "default"
KEYWARDEN_AGENT_CERT_VALIDITY_DAYS = int(os.getenv("KEYWARDEN_AGENT_CERT_VALIDITY_DAYS", "90")) KEYWARDEN_AGENT_CERT_VALIDITY_DAYS = int(os.getenv("KEYWARDEN_AGENT_CERT_VALIDITY_DAYS", "90"))
KEYWARDEN_USER_CERT_VALIDITY_DAYS = int(os.getenv("KEYWARDEN_USER_CERT_VALIDITY_DAYS", "30")) KEYWARDEN_USER_CERT_VALIDITY_DAYS = int(os.getenv("KEYWARDEN_USER_CERT_VALIDITY_DAYS", "30"))
KEYWARDEN_SHELL_CERT_VALIDITY_MINUTES = int(os.getenv("KEYWARDEN_SHELL_CERT_VALIDITY_MINUTES", "15"))
KEYWARDEN_ACCOUNT_USERNAME_TEMPLATE = os.getenv( KEYWARDEN_ACCOUNT_USERNAME_TEMPLATE = os.getenv(
"KEYWARDEN_ACCOUNT_USERNAME_TEMPLATE", "{{username}}_{{user_id}}" "KEYWARDEN_ACCOUNT_USERNAME_TEMPLATE", "{{username}}_{{user_id}}"
) )