✨ feat(federation): dev-only insecure flag for local two-host trials
Changes
5 files changed, +81 -4
MODIFY
README.md
+1 -0
@@ -70,6 +70,7 @@
70
70
| `GITSHARK_FEDERATION_BASE_URL` | — | Public HTTPS origin (e.g. `https://shark.example`); actor IDs derive from it and are permanent |
71
71
| `GITSHARK_FEDERATION_PEER_ALLOWLIST` | — (empty = deny all) | Comma-separated peer hosts allowed to send/receive federation traffic |
72
72
| `GITSHARK_FEDERATION_MAX_ATTEMPTS` | `8` | Max delivery attempts before a queued activity is dead-lettered |
73
+| `GITSHARK_FEDERATION_DEV_ALLOW_INSECURE` | `false` | **Dev/local only.** Lets the SSRF guard accept `http` + loopback/private targets so two instances can federate on one machine (peer allowlist still enforced). Never enable in production. |
73
74
74
75
> **TLS required in production:** personal access tokens travel as HTTP Basic credentials.
75
76
> Terminate TLS in front of the service; never expose plain HTTP publicly.
MODIFY
src/main/java/de/workaround/federation/FederationConfig.java
+13 -0
@@ -31,6 +31,9 @@
31
31
@ConfigProperty(name = "gitshark.federation.delivery.max-attempts", defaultValue = "8")
32
32
int maxDeliveryAttempts;
33
33
34
+ @ConfigProperty(name = "gitshark.federation.dev-allow-insecure", defaultValue = "false")
35
+ boolean devAllowInsecure;
36
+
34
37
public boolean enabled()
35
38
{
36
39
return enabled;
@@ -72,6 +75,16 @@
72
75
return maxDeliveryAttempts;
73
76
}
74
77
78
+ /**
79
+ * Dev/local-only escape hatch: when true, the SSRF guard permits {@code http} and
80
+ * loopback/private targets (the peer allowlist is still enforced). Never enable in production —
81
+ * it exists so two instances can federate on one machine for testing.
82
+ */
83
+ public boolean devAllowInsecure()
84
+ {
85
+ return devAllowInsecure;
86
+ }
87
+
75
88
/** Whether the given host is on the peer allowlist (case-insensitive). */
76
89
public boolean peerAllowed(String host)
77
90
{
MODIFY
src/main/java/de/workaround/federation/RemoteUrlGuard.java
+12 -4
@@ -38,7 +38,11 @@
38
38
{
39
39
throw new UnsafeUrlException("Malformed URL: " + url);
40
40
}
41
- if (uri.getScheme() == null || !uri.getScheme().equalsIgnoreCase("https"))
41
+ boolean insecure = config.devAllowInsecure();
42
+ String scheme = uri.getScheme();
43
+ boolean schemeOk = scheme != null
44
+ && (scheme.equalsIgnoreCase("https") || (insecure && scheme.equalsIgnoreCase("http")));
45
+ if (!schemeOk)
42
46
{
43
47
throw new UnsafeUrlException("Only https is allowed: " + url);
44
48
}
@@ -47,15 +51,19 @@
47
51
{
48
52
throw new UnsafeUrlException("Missing host: " + url);
49
53
}
54
+ // The peer allowlist is always enforced, even in dev-insecure mode.
50
55
if (!config.peerAllowed(host))
51
56
{
52
57
throw new UnsafeUrlException("Host not on peer allowlist: " + host);
53
58
}
54
- for (InetAddress address : resolve(host))
59
+ if (!insecure)
55
60
{
56
- if (isNonPublic(address))
61
+ for (InetAddress address : resolve(host))
57
62
{
58
- throw new UnsafeUrlException("Host resolves to a non-public address: " + host);
63
+ if (isNonPublic(address))
64
+ {
65
+ throw new UnsafeUrlException("Host resolves to a non-public address: " + host);
66
+ }
59
67
}
60
68
}
61
69
return uri;
MODIFY
src/main/resources/application.properties
+2 -0
@@ -38,6 +38,8 @@
38
38
gitshark.federation.base-url=${GITSHARK_FEDERATION_BASE_URL:}
39
39
gitshark.federation.peer-allowlist=${GITSHARK_FEDERATION_PEER_ALLOWLIST:}
40
40
gitshark.federation.delivery.max-attempts=${GITSHARK_FEDERATION_MAX_ATTEMPTS:8}
41
+# Dev/local ONLY: allow http + loopback/private federation targets (allowlist still enforced).
42
+gitshark.federation.dev-allow-insecure=${GITSHARK_FEDERATION_DEV_ALLOW_INSECURE:false}
41
43
%test.gitshark.federation.enabled=true
42
44
%test.gitshark.federation.base-url=https://shark.test
43
45
%test.gitshark.federation.peer-allowlist=peer.test,shark.test
ADD
src/test/java/de/workaround/federation/RemoteUrlGuardInsecureTest.java
+53 -0
@@ -0,0 +1,53 @@
1
+package de.workaround.federation;
2
+
3
+import java.util.Map;
4
+
5
+import org.junit.jupiter.api.Test;
6
+
7
+import io.quarkus.test.junit.QuarkusTest;
8
+import io.quarkus.test.junit.TestProfile;
9
+import io.quarkus.test.junit.QuarkusTestProfile;
10
+import jakarta.inject.Inject;
11
+
12
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
13
+import static org.junit.jupiter.api.Assertions.assertThrows;
14
+
15
+/**
16
+ * With the dev-only insecure flag on, the SSRF guard permits http + loopback targets (so two
17
+ * instances can federate on one machine) but STILL enforces the peer allowlist.
18
+ */
19
+@QuarkusTest
20
+@TestProfile(RemoteUrlGuardInsecureTest.InsecureProfile.class)
21
+class RemoteUrlGuardInsecureTest
22
+{
23
+ public static class InsecureProfile implements QuarkusTestProfile
24
+ {
25
+ @Override
26
+ public Map<String, String> getConfigOverrides()
27
+ {
28
+ return Map.of(
29
+ "gitshark.federation.enabled", "true",
30
+ "gitshark.federation.base-url", "https://shark.test",
31
+ "gitshark.federation.dev-allow-insecure", "true",
32
+ "gitshark.federation.peer-allowlist", "localhost,127.0.0.1,shark-b.local");
33
+ }
34
+ }
35
+
36
+ @Inject
37
+ RemoteUrlGuard guard;
38
+
39
+ @Test
40
+ void httpAndLoopbackAllowedWhenInsecureAndAllowlisted()
41
+ {
42
+ assertDoesNotThrow(() -> guard.requireSafe("http://localhost:8081/ap/instance/inbox"));
43
+ assertDoesNotThrow(() -> guard.requireSafe("https://shark-b.local:8443/ap/instance/inbox"));
44
+ }
45
+
46
+ @Test
47
+ void allowlistStillEnforcedInInsecureMode()
48
+ {
49
+ assertThrows(RemoteUrlGuard.UnsafeUrlException.class,
50
+ () -> guard.requireSafe("http://evil.example/ap/users/x"));
51
+ }
52
+
53
+}