Revert "sign_ssh_keys: Add hook to sign SSH host cert"
No longer using Step CA for SSH host certificates. Switched to sshca.
This reverts commit e5eff964a1
.
master
parent
e5eff964a1
commit
55df6f61a7
|
@ -1,4 +1,3 @@
|
||||||
/.venv
|
/.venv
|
||||||
__pycache__/
|
__pycache__/
|
||||||
*.egg-info/
|
*.egg-info/
|
||||||
provisioner.password
|
|
||||||
|
|
|
@ -17,12 +17,9 @@ RUN --mount=type=cache,target=/var/cache/apt \
|
||||||
RUN --mount=from=build,source=/tmp/build/dist,target=/tmp/wheels \
|
RUN --mount=from=build,source=/tmp/build/dist,target=/tmp/wheels \
|
||||||
python3 -m pip install -f /tmp/wheels \
|
python3 -m pip install -f /tmp/wheels \
|
||||||
dch_webhooks \
|
dch_webhooks \
|
||||||
python-multipart \
|
|
||||||
uvicorn \
|
uvicorn \
|
||||||
&& :
|
&& :
|
||||||
|
|
||||||
COPY --from=docker.io/smallstep/step-cli:0.25.0 /usr/local/bin/step /usr/local/bin/step
|
|
||||||
|
|
||||||
USER 1000:1000
|
USER 1000:1000
|
||||||
|
|
||||||
CMD ["tini", "/usr/local/bin/uvicorn", "dch_webhooks:app"]
|
CMD ["tini", "/usr/local/bin/uvicorn", "dch_webhooks:app"]
|
||||||
|
|
131
dch_webhooks.py
131
dch_webhooks.py
|
@ -1,14 +1,10 @@
|
||||||
import asyncio
|
|
||||||
import base64
|
|
||||||
import datetime
|
import datetime
|
||||||
import importlib.metadata
|
import importlib.metadata
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import tempfile
|
|
||||||
from pathlib import Path
|
|
||||||
from types import TracebackType
|
from types import TracebackType
|
||||||
from typing import Annotated, Optional, Self, Type
|
from typing import Optional, Self, Type
|
||||||
|
|
||||||
import fastapi
|
import fastapi
|
||||||
import httpx
|
import httpx
|
||||||
|
@ -38,17 +34,10 @@ EXCLUDE_DESCRIPTION_WORDS = {
|
||||||
'the',
|
'the',
|
||||||
}
|
}
|
||||||
|
|
||||||
ALLOW_RESIGNING_CERTS = os.environ.get('ALLOW_RESIGNING_CERTS') == '1'
|
|
||||||
FIREFLY_URL = os.environ.get(
|
FIREFLY_URL = os.environ.get(
|
||||||
'FIREFLY_URL',
|
'FIREFLY_URL',
|
||||||
'http://firefly-iii',
|
'http://firefly-iii',
|
||||||
)
|
)
|
||||||
MAX_KEY_FILE_SIZE = int(
|
|
||||||
os.environ.get(
|
|
||||||
'MAX_KEY_FILE_SIZE',
|
|
||||||
8192,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
MAX_DOCUMENT_SIZE = int(
|
MAX_DOCUMENT_SIZE = int(
|
||||||
os.environ.get(
|
os.environ.get(
|
||||||
'MAX_DOCUMENT_SIZE',
|
'MAX_DOCUMENT_SIZE',
|
||||||
|
@ -61,10 +50,6 @@ PAPERLESS_URL = os.environ.get(
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class SignError(Exception):
|
|
||||||
...
|
|
||||||
|
|
||||||
|
|
||||||
class FireflyIIITransactionSplit(pydantic.BaseModel):
|
class FireflyIIITransactionSplit(pydantic.BaseModel):
|
||||||
type: str
|
type: str
|
||||||
date: datetime.datetime
|
date: datetime.datetime
|
||||||
|
@ -93,12 +78,6 @@ class PaperlessNgxSearchResults(pydantic.BaseModel):
|
||||||
results: list[PaperlessNgxDocument]
|
results: list[PaperlessNgxDocument]
|
||||||
|
|
||||||
|
|
||||||
class SSHKeySignResponse(pydantic.BaseModel):
|
|
||||||
success: bool
|
|
||||||
errors: Optional[list[str]] = None
|
|
||||||
certificates: Optional[dict[str, str]]
|
|
||||||
|
|
||||||
|
|
||||||
class HttpxClientMixin:
|
class HttpxClientMixin:
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
@ -165,13 +144,9 @@ class Firefly(HttpxClientMixin):
|
||||||
rbody = r.json()
|
rbody = r.json()
|
||||||
attachment = rbody['data']
|
attachment = rbody['data']
|
||||||
url = f'{FIREFLY_URL}/api/v1/attachments/{attachment["id"]}/upload'
|
url = f'{FIREFLY_URL}/api/v1/attachments/{attachment["id"]}/upload'
|
||||||
r = await self.client.post(
|
r = await self.client.post(url, content=doc, headers={
|
||||||
url,
|
'Content-Type': 'application/octet-stream',
|
||||||
content=doc,
|
})
|
||||||
headers={
|
|
||||||
'Content-Type': 'application/octet-stream',
|
|
||||||
},
|
|
||||||
)
|
|
||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
|
|
||||||
|
|
||||||
|
@ -272,7 +247,9 @@ class Paperless(HttpxClientMixin):
|
||||||
MAX_DOCUMENT_SIZE,
|
MAX_DOCUMENT_SIZE,
|
||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
docs.append((response_filename(r), doc.title, await r.aread()))
|
docs.append(
|
||||||
|
(response_filename(r), doc.title, await r.aread())
|
||||||
|
)
|
||||||
return docs
|
return docs
|
||||||
|
|
||||||
|
|
||||||
|
@ -302,12 +279,6 @@ async def handle_firefly_transaction(xact: FireflyIIITransaction) -> None:
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def check_host(hostname: str) -> bool:
|
|
||||||
cmd = ['step', 'ssh', 'check-host', hostname]
|
|
||||||
p = await asyncio.create_subprocess_exec(*cmd)
|
|
||||||
return await p.wait() == 0
|
|
||||||
|
|
||||||
|
|
||||||
def clean_description(text: str) -> str:
|
def clean_description(text: str) -> str:
|
||||||
matches = DESCRIPTION_CLEAN_PATTERN.sub('', text.lower())
|
matches = DESCRIPTION_CLEAN_PATTERN.sub('', text.lower())
|
||||||
if not matches:
|
if not matches:
|
||||||
|
@ -337,56 +308,6 @@ def response_filename(response: httpx.Response) -> str:
|
||||||
return response.url.path.rstrip('/').rsplit('/', 1)[-1]
|
return response.url.path.rstrip('/').rsplit('/', 1)[-1]
|
||||||
|
|
||||||
|
|
||||||
async def sign_key(hostname, path: Path) -> tuple[str, str]:
|
|
||||||
cmd = ['step', 'ssh', 'certificate', '--sign', '--host', hostname, path]
|
|
||||||
p = await asyncio.create_subprocess_exec(
|
|
||||||
*cmd,
|
|
||||||
stdin=asyncio.subprocess.DEVNULL,
|
|
||||||
stdout=asyncio.subprocess.PIPE,
|
|
||||||
stderr=asyncio.subprocess.STDOUT,
|
|
||||||
)
|
|
||||||
p_log = log.getChild('step')
|
|
||||||
assert p.stdout
|
|
||||||
buf = bytearray()
|
|
||||||
while line := await p.stdout.readline():
|
|
||||||
buf += line
|
|
||||||
p_log.info(line.rstrip().decode('utf-8', 'replace'))
|
|
||||||
rc = await p.wait()
|
|
||||||
if rc != 0:
|
|
||||||
raise SignError(
|
|
||||||
f'Signing failed: process returned exit code {rc}: '
|
|
||||||
f'{buf.decode("utf-8")}'
|
|
||||||
)
|
|
||||||
cert_path = path.parent / f'{path.stem}-cert.pub'
|
|
||||||
log.info(
|
|
||||||
'Successfully signed %s for %s as %s',
|
|
||||||
path.name,
|
|
||||||
hostname,
|
|
||||||
cert_path.name,
|
|
||||||
)
|
|
||||||
with cert_path.open('r') as f:
|
|
||||||
cert = await asyncio.to_thread(f.read)
|
|
||||||
return (cert_path.name, cert)
|
|
||||||
|
|
||||||
|
|
||||||
async def sign_uploaded_key(
|
|
||||||
hostname: str, f: fastapi.UploadFile
|
|
||||||
) -> tuple[str, str]:
|
|
||||||
if f.size > MAX_KEY_FILE_SIZE:
|
|
||||||
raise SignError(
|
|
||||||
f'Refusing to sign key {f.filename}: file too large '
|
|
||||||
f'({f.size} bytes, max {MAX_KEY_FILE_SIZE}'
|
|
||||||
)
|
|
||||||
with tempfile.TemporaryDirectory() as t:
|
|
||||||
path = Path(t) / f.filename
|
|
||||||
with path.open('wb') as o:
|
|
||||||
d = await f.read(MAX_KEY_FILE_SIZE)
|
|
||||||
if f.headers.get('Content-Transfer-Encoding') == 'base64':
|
|
||||||
d = base64.b64decode(d)
|
|
||||||
await asyncio.to_thread(o.write, d)
|
|
||||||
return await sign_key(hostname, path)
|
|
||||||
|
|
||||||
|
|
||||||
app = fastapi.FastAPI(
|
app = fastapi.FastAPI(
|
||||||
name=DIST['Name'],
|
name=DIST['Name'],
|
||||||
version=DIST['Version'],
|
version=DIST['Version'],
|
||||||
|
@ -410,41 +331,3 @@ def status() -> str:
|
||||||
@app.post('/hooks/firefly-iii/create')
|
@app.post('/hooks/firefly-iii/create')
|
||||||
async def firefly_iii_create(hook: FireflyIIIWebhook) -> None:
|
async def firefly_iii_create(hook: FireflyIIIWebhook) -> None:
|
||||||
await handle_firefly_transaction(hook.content)
|
await handle_firefly_transaction(hook.content)
|
||||||
|
|
||||||
|
|
||||||
@app.post('/sshkeys/sign', response_model=SSHKeySignResponse)
|
|
||||||
async def sign_ssh_keys(
|
|
||||||
response: fastapi.Response,
|
|
||||||
hostname: Annotated[str, fastapi.Form()],
|
|
||||||
keys: list[fastapi.UploadFile],
|
|
||||||
):
|
|
||||||
errors = []
|
|
||||||
certificates = {}
|
|
||||||
if '.' not in hostname:
|
|
||||||
errors.append(
|
|
||||||
f'Cannot sign certificate for Single-label hostname {hostname}'
|
|
||||||
)
|
|
||||||
if await check_host(hostname):
|
|
||||||
msg = f'{hostname} already has a signed certificate'
|
|
||||||
if ALLOW_RESIGNING_CERTS:
|
|
||||||
log.warning('%s', msg)
|
|
||||||
else:
|
|
||||||
log.error('%s', msg)
|
|
||||||
errors.append(msg)
|
|
||||||
if not errors:
|
|
||||||
tasks = [sign_uploaded_key(hostname, k) for k in keys]
|
|
||||||
for coro in asyncio.as_completed(tasks):
|
|
||||||
try:
|
|
||||||
name, cert = await coro
|
|
||||||
except Exception as e:
|
|
||||||
log.error('%s', e)
|
|
||||||
errors.append(str(e))
|
|
||||||
else:
|
|
||||||
certificates[name] = cert
|
|
||||||
if errors:
|
|
||||||
response.status_code = fastapi.status.HTTP_400_BAD_REQUEST
|
|
||||||
return SSHKeySignResponse(
|
|
||||||
success=not errors,
|
|
||||||
errors=errors or None,
|
|
||||||
certificates=certificates or None,
|
|
||||||
)
|
|
||||||
|
|
Loading…
Reference in New Issue