Harden the remote worker: TLS off loopback, full digests, filtered env
fluksio-worker now refuses ws:// and http:// engine and artifact URLs unless they are loopback or --insecure is given; the engine still accepts both. An artifact reference must be sha256: plus 64 hex digits before it is joined into a path, and node code gets the host environment through the engine's credential denylist instead of all of it.
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
"""The remote worker's own guards: where its token goes, and what node code gets."""
|
||||
|
||||
import argparse
|
||||
|
||||
import pytest
|
||||
from fluksio_worker import agent
|
||||
from fluksio_worker.worker_main import _Reporter
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("url", "trusted"),
|
||||
[
|
||||
("wss://engine.example.com/api/v1/workers/attach", True),
|
||||
("https://engine.example.com/api/v1/artifacts", True),
|
||||
("ws://127.0.0.1:8000/api/v1/workers/attach", True),
|
||||
("ws://127.1.2.3:8000/api/v1/workers/attach", True),
|
||||
("ws://[::1]:8000/api/v1/workers/attach", True),
|
||||
("ws://localhost:8000/api/v1/workers/attach", True),
|
||||
("ws://192.0.2.10:8000/api/v1/workers/attach", False),
|
||||
("ws://engine.local:8000/api/v1/workers/attach", False),
|
||||
("http://192.0.2.10:8000/api/v1/artifacts", False),
|
||||
],
|
||||
)
|
||||
def test_only_tls_or_this_machine_is_handed_the_token(url: str, trusted: bool) -> None:
|
||||
assert agent._encrypted_or_local(url) is trusted
|
||||
|
||||
|
||||
def test_a_cleartext_engine_elsewhere_needs_insecure(
|
||||
monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
||||
) -> None:
|
||||
url = "ws://192.0.2.10:8000/api/v1/workers/attach"
|
||||
with pytest.raises(SystemExit):
|
||||
agent.main(["--url", url, "--token", "t"])
|
||||
assert "--insecure" in capsys.readouterr().err
|
||||
|
||||
async def attached(self: agent.Agent) -> None:
|
||||
return None
|
||||
|
||||
monkeypatch.setattr(agent.Agent, "serve_forever", attached)
|
||||
assert agent.main(["--url", url, "--token", "t", "--insecure"]) == 0
|
||||
|
||||
|
||||
def test_node_code_does_not_get_the_hosts_credentials(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
monkeypatch.setenv("FLUKSIO_WORKER_TOKEN", "worker-token")
|
||||
monkeypatch.setenv("AWS_SECRET_ACCESS_KEY", "not-for-nodes")
|
||||
monkeypatch.setenv("CUDA_VISIBLE_DEVICES", "2")
|
||||
args = argparse.Namespace(
|
||||
url="wss://engine.example.com/api/v1/workers/attach",
|
||||
artifact_url="",
|
||||
token="worker-token",
|
||||
)
|
||||
|
||||
env = agent.Agent(args).env
|
||||
|
||||
assert "FLUKSIO_WORKER_TOKEN" not in env
|
||||
assert "AWS_SECRET_ACCESS_KEY" not in env
|
||||
assert env["CUDA_VISIBLE_DEVICES"] == "2"
|
||||
assert "PATH" in env
|
||||
# Put back on purpose: the artifact store is reached with it.
|
||||
assert env["FLUKSIO_ARTIFACT_URL"] == "https://engine.example.com/api/v1/artifacts"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"digest",
|
||||
[
|
||||
"sha256:../../../etc/passwd",
|
||||
"sha256:" + "a" * 63,
|
||||
"sha256:" + "A" * 64,
|
||||
"md5:" + "a" * 64,
|
||||
],
|
||||
)
|
||||
def test_only_a_whole_digest_is_opened(digest: str) -> None:
|
||||
"""The digest is joined into a path, so anything else could read any file."""
|
||||
with pytest.raises(ValueError, match="not an artifact reference"):
|
||||
_Reporter("fluksio").load_artifact({"digest": digest})
|
||||
Reference in New Issue
Block a user