Skip to content

Commit d6deede

Browse files
GitHKAndrei Neagu
andauthored
🎨 docker-api-proxy always requires authentication (⚠️devops) (#7586)
Co-authored-by: Andrei Neagu <[email protected]>
1 parent 9d62136 commit d6deede

File tree

14 files changed

+118
-187
lines changed

14 files changed

+118
-187
lines changed

.env-devel

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,10 +86,10 @@ DIRECTOR_SERVICES_CUSTOM_CONSTRAINTS=null
8686
DIRECTOR_TRACING=null
8787

8888
DOCKER_API_PROXY_HOST=docker-api-proxy
89-
DOCKER_API_PROXY_PASSWORD=null
89+
DOCKER_API_PROXY_PASSWORD=admin
9090
DOCKER_API_PROXY_PORT=8888
9191
DOCKER_API_PROXY_SECURE=False
92-
DOCKER_API_PROXY_USER=null
92+
DOCKER_API_PROXY_USER=admin
9393

9494
EFS_USER_ID=8006
9595
EFS_USER_NAME=efs

packages/pytest-simcore/src/pytest_simcore/docker_api_proxy.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import logging
22

33
import pytest
4-
from aiohttp import ClientSession, ClientTimeout
4+
from aiohttp import BasicAuth, ClientSession, ClientTimeout
55
from pydantic import TypeAdapter
66
from settings_library.docker_api_proxy import DockerApiProxysettings
77
from tenacity import before_sleep_log, retry, stop_after_delay, wait_fixed
@@ -22,7 +22,13 @@
2222
async def _wait_till_docker_api_proxy_is_responsive(
2323
settings: DockerApiProxysettings,
2424
) -> None:
25-
async with ClientSession(timeout=ClientTimeout(1, 1, 1, 1, 1)) as client:
25+
async with ClientSession(
26+
timeout=ClientTimeout(total=1),
27+
auth=BasicAuth(
28+
settings.DOCKER_API_PROXY_USER,
29+
settings.DOCKER_API_PROXY_PASSWORD.get_secret_value(),
30+
),
31+
) as client:
2632
response = await client.get(f"{settings.base_url}/version")
2733
assert response.status == 200, await response.text()
2834

@@ -44,6 +50,12 @@ async def docker_api_proxy_settings(
4450
{
4551
"DOCKER_API_PROXY_HOST": get_localhost_ip(),
4652
"DOCKER_API_PROXY_PORT": published_port,
53+
"DOCKER_API_PROXY_USER": env_vars_for_docker_compose[
54+
"DOCKER_API_PROXY_USER"
55+
],
56+
"DOCKER_API_PROXY_PASSWORD": env_vars_for_docker_compose[
57+
"DOCKER_API_PROXY_PASSWORD"
58+
],
4759
}
4860
)
4961

packages/pytest-simcore/src/pytest_simcore/simcore_services.py

Lines changed: 29 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from collections.abc import Iterator
1010
from dataclasses import dataclass
1111
from io import StringIO
12+
from typing import Final
1213

1314
import aiohttp
1415
import pytest
@@ -27,7 +28,7 @@
2728
log = logging.getLogger(__name__)
2829

2930

30-
_SERVICES_TO_SKIP = {
31+
_SERVICES_TO_SKIP: Final[set[str]] = {
3132
"agent", # global mode deploy (NO exposed ports, has http API)
3233
"dask-sidecar", # global mode deploy (NO exposed ports, **NO** http API)
3334
"migration",
@@ -41,9 +42,8 @@
4142
"sto-worker-cpu-bound",
4243
}
4344
# TODO: unify healthcheck policies see https://github.com/ITISFoundation/osparc-simcore/pull/2281
44-
SERVICE_PUBLISHED_PORT = {}
45-
DEFAULT_SERVICE_HEALTHCHECK_ENTRYPOINT = "/v0/"
46-
MAP_SERVICE_HEALTHCHECK_ENTRYPOINT = {
45+
DEFAULT_SERVICE_HEALTHCHECK_ENTRYPOINT: Final[str] = "/v0/"
46+
MAP_SERVICE_HEALTHCHECK_ENTRYPOINT: Final[dict[str, str]] = {
4747
"autoscaling": "/",
4848
"clusters-keeper": "/",
4949
"dask-scheduler": "/health",
@@ -57,16 +57,23 @@
5757
"resource-usage-tracker": "/",
5858
"docker-api-proxy": "/version",
5959
}
60-
AIOHTTP_BASED_SERVICE_PORT: int = 8080
61-
FASTAPI_BASED_SERVICE_PORT: int = 8000
62-
DASK_SCHEDULER_SERVICE_PORT: int = 8787
63-
DOCKER_API_PROXY_SERVICE_PORT: int = 8888
6460

65-
_SERVICE_NAME_REPLACEMENTS: dict[str, str] = {
61+
# some services require authentication to access their health-check endpoints
62+
_BASE_AUTH_ENV_VARS: Final[dict[str, tuple[str, str]]] = {
63+
"docker-api-proxy": ("DOCKER_API_PROXY_USER", "DOCKER_API_PROXY_PASSWORD"),
64+
}
65+
66+
_SERVICE_NAME_REPLACEMENTS: Final[dict[str, str]] = {
6667
"dynamic-scheduler": "dynamic-schdlr",
6768
}
6869

69-
_ONE_SEC_TIMEOUT = ClientTimeout(total=1) # type: ignore
70+
71+
AIOHTTP_BASED_SERVICE_PORT: Final[int] = 8080
72+
FASTAPI_BASED_SERVICE_PORT: Final[int] = 8000
73+
DASK_SCHEDULER_SERVICE_PORT: Final[int] = 8787
74+
DOCKER_API_PROXY_SERVICE_PORT: Final[int] = 8888
75+
76+
_ONE_SEC_TIMEOUT: Final[ClientTimeout] = ClientTimeout(total=1) # type: ignore
7077

7178

7279
async def wait_till_service_healthy(service_name: str, endpoint: URL):
@@ -108,13 +115,12 @@ class ServiceHealthcheckEndpoint:
108115
@classmethod
109116
def create(cls, service_name: str, baseurl):
110117
# TODO: unify healthcheck policies see https://github.com/ITISFoundation/osparc-simcore/pull/2281
111-
obj = cls(
118+
return cls(
112119
name=service_name,
113120
url=URL(
114121
f"{baseurl}{MAP_SERVICE_HEALTHCHECK_ENTRYPOINT.get(service_name, DEFAULT_SERVICE_HEALTHCHECK_ENTRYPOINT)}"
115122
),
116123
)
117-
return obj
118124

119125

120126
@pytest.fixture(scope="module")
@@ -140,9 +146,17 @@ def services_endpoint(
140146
DASK_SCHEDULER_SERVICE_PORT,
141147
DOCKER_API_PROXY_SERVICE_PORT,
142148
]
143-
endpoint = URL(
144-
f"http://{get_localhost_ip()}:{get_service_published_port(full_service_name, target_ports)}"
145-
)
149+
if service in _BASE_AUTH_ENV_VARS:
150+
user_env, password_env = _BASE_AUTH_ENV_VARS[service]
151+
user = env_vars_for_docker_compose[user_env]
152+
password = env_vars_for_docker_compose[password_env]
153+
endpoint = URL(
154+
f"http://{user}:{password}@{get_localhost_ip()}:{get_service_published_port(full_service_name, target_ports)}"
155+
)
156+
else:
157+
endpoint = URL(
158+
f"http://{get_localhost_ip()}:{get_service_published_port(full_service_name, target_ports)}"
159+
)
146160
services_endpoint[service] = endpoint
147161
else:
148162
print(f"Collecting service endpoints: '{service}' skipped")

packages/service-library/src/servicelib/fastapi/docker.py

Lines changed: 7 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -30,32 +30,20 @@ async def remote_docker_client_lifespan(
3030
) -> AsyncIterator[State]:
3131
settings: DockerApiProxysettings = state[_DOCKER_API_PROXY_SETTINGS]
3232

33-
session: ClientSession | None = None
34-
if settings.DOCKER_API_PROXY_USER and settings.DOCKER_API_PROXY_PASSWORD:
35-
session = ClientSession(
36-
auth=aiohttp.BasicAuth(
37-
login=settings.DOCKER_API_PROXY_USER,
38-
password=settings.DOCKER_API_PROXY_PASSWORD.get_secret_value(),
39-
)
40-
)
41-
4233
async with AsyncExitStack() as exit_stack:
43-
if settings.DOCKER_API_PROXY_USER and settings.DOCKER_API_PROXY_PASSWORD:
44-
await exit_stack.enter_async_context(
45-
ClientSession(
46-
auth=aiohttp.BasicAuth(
47-
login=settings.DOCKER_API_PROXY_USER,
48-
password=settings.DOCKER_API_PROXY_PASSWORD.get_secret_value(),
49-
)
34+
session = await exit_stack.enter_async_context(
35+
ClientSession(
36+
auth=aiohttp.BasicAuth(
37+
login=settings.DOCKER_API_PROXY_USER,
38+
password=settings.DOCKER_API_PROXY_PASSWORD.get_secret_value(),
5039
)
5140
)
41+
)
5242

53-
client = await exit_stack.enter_async_context(
43+
app.state.remote_docker_client = await exit_stack.enter_async_context(
5444
aiodocker.Docker(url=settings.base_url, session=session)
5545
)
5646

57-
app.state.remote_docker_client = client
58-
5947
await wait_till_docker_api_proxy_is_responsive(app)
6048

6149
# NOTE this has to be inside exit_stack scope

packages/settings-library/src/settings_library/docker_api_proxy.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ class DockerApiProxysettings(BaseCustomSettings):
1515
)
1616
DOCKER_API_PROXY_SECURE: bool = False
1717

18-
DOCKER_API_PROXY_USER: str | None = None
19-
DOCKER_API_PROXY_PASSWORD: SecretStr | None = None
18+
DOCKER_API_PROXY_USER: str
19+
DOCKER_API_PROXY_PASSWORD: SecretStr
2020

2121
@cached_property
2222
def base_url(self) -> str:

services/docker-api-proxy/Dockerfile

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
FROM alpine:3.21 AS base
1+
FROM caddy:2.10.0-alpine AS base
22

33
LABEL maintainer=GitHK
44

@@ -29,10 +29,11 @@ HEALTHCHECK \
2929
--start-period=20s \
3030
--start-interval=1s \
3131
--retries=5 \
32-
CMD curl http://localhost:8888/version || exit 1
32+
CMD curl --fail-with-body --user ${DOCKER_API_PROXY_USER}:${DOCKER_API_PROXY_PASSWORD} http://localhost:8888/version
3333

3434
COPY --chown=scu:scu services/docker-api-proxy/docker services/docker-api-proxy/docker
35-
RUN chmod +x services/docker-api-proxy/docker/*.sh
35+
RUN chmod +x services/docker-api-proxy/docker/*.sh && \
36+
mv services/docker-api-proxy/docker/Caddyfile /etc/caddy/Caddyfile
3637

3738
ENTRYPOINT [ "/bin/sh", "services/docker-api-proxy/docker/entrypoint.sh" ]
3839
CMD ["/bin/sh", "services/docker-api-proxy/docker/boot.sh"]
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
:8888 {
2+
handle {
3+
basicauth {
4+
{$DOCKER_API_PROXY_USER} {$DOCKER_API_PROXY_ENCRYPTED_PASSWORD}
5+
}
6+
7+
reverse_proxy http://localhost:8889 {
8+
health_uri /version
9+
}
10+
}
11+
}

services/docker-api-proxy/docker/boot.sh

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,7 @@ echo "$INFO" "User :$(id "$(whoami)")"
1212
#
1313
# RUNNING application
1414
#
15-
socat TCP-LISTEN:8888,fork,reuseaddr UNIX-CONNECT:/var/run/docker.sock
15+
socat TCP-LISTEN:8889,fork,reuseaddr UNIX-CONNECT:/var/run/docker.sock &
16+
17+
DOCKER_API_PROXY_ENCRYPTED_PASSWORD=$(caddy hash-password --plaintext "$DOCKER_API_PROXY_PASSWORD") \
18+
caddy run --adapter caddyfile --config /etc/caddy/Caddyfile

services/docker-api-proxy/tests/integration/autentication-proxy-docker-compose.yaml

Lines changed: 0 additions & 14 deletions
This file was deleted.
Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,21 @@
11
# pylint: disable=unused-argument
22

33
import json
4-
import sys
54
from collections.abc import Callable
65
from contextlib import AbstractAsyncContextManager
7-
from pathlib import Path
86

97
import aiodocker
8+
import pytest
109
from pytest_simcore.helpers.monkeypatch_envs import EnvVarsDict
10+
from servicelib.aiohttp import status
1111
from settings_library.docker_api_proxy import DockerApiProxysettings
1212

13-
HERE = Path(sys.argv[0] if __name__ == "__main__" else __file__).resolve().parent
14-
1513
pytest_simcore_core_services_selection = [
1614
"docker-api-proxy",
1715
]
1816

1917

20-
async def test_unauthenticated_docker_client(
18+
async def test_authenticated_docker_client(
2119
docker_swarm: None,
2220
docker_api_proxy_settings: DockerApiProxysettings,
2321
setup_docker_client: Callable[
@@ -27,7 +25,37 @@ async def test_unauthenticated_docker_client(
2725
envs = {
2826
"DOCKER_API_PROXY_HOST": "127.0.0.1",
2927
"DOCKER_API_PROXY_PORT": "8014",
28+
"DOCKER_API_PROXY_USER": docker_api_proxy_settings.DOCKER_API_PROXY_USER,
29+
"DOCKER_API_PROXY_PASSWORD": docker_api_proxy_settings.DOCKER_API_PROXY_PASSWORD.get_secret_value(),
3030
}
3131
async with setup_docker_client(envs) as working_docker:
3232
info = await working_docker.system.info()
3333
print(json.dumps(info, indent=2))
34+
35+
36+
@pytest.mark.parametrize(
37+
"user, password",
38+
[
39+
("wrong", "wrong"),
40+
("wrong", "admin"),
41+
],
42+
)
43+
async def test_unauthenticated_docker_client(
44+
docker_swarm: None,
45+
docker_api_proxy_settings: DockerApiProxysettings,
46+
setup_docker_client: Callable[
47+
[EnvVarsDict], AbstractAsyncContextManager[aiodocker.Docker]
48+
],
49+
user: str,
50+
password: str,
51+
):
52+
envs = {
53+
"DOCKER_API_PROXY_HOST": "127.0.0.1",
54+
"DOCKER_API_PROXY_PORT": "8014",
55+
"DOCKER_API_PROXY_USER": user,
56+
"DOCKER_API_PROXY_PASSWORD": password,
57+
}
58+
async with setup_docker_client(envs) as working_docker:
59+
with pytest.raises(aiodocker.exceptions.DockerError) as exc:
60+
await working_docker.system.info()
61+
assert exc.value.status == status.HTTP_401_UNAUTHORIZED

0 commit comments

Comments
 (0)