feat(deploy): provision Unscouted smoke tenant (#22)

This commit is contained in:
KyuubiYoru
2026-07-16 21:49:17 +02:00
parent 9e863ebf64
commit f368fec6eb
4 changed files with 92 additions and 7 deletions
@@ -32,6 +32,16 @@
"NotBefore": "2026-01-01T00:00:00Z", "NotBefore": "2026-01-01T00:00:00Z",
"SignUntil": "2100-01-01T00:00:00Z", "SignUntil": "2100-01-01T00:00:00Z",
"VerifyUntil": "2100-01-02T00:00:00Z" "VerifyUntil": "2100-01-02T00:00:00Z"
},
{
"KeyId": "local-smoke-unscouted-1",
"SecretReference": "file:/run/secrets/rendezvous-signing-key",
"CredentialKinds": ["DedicatedPublisher"],
"GameId": "unscouted",
"EnvironmentId": "smoke",
"NotBefore": "2026-01-01T00:00:00Z",
"SignUntil": "2100-01-01T00:00:00Z",
"VerifyUntil": "2100-01-02T00:00:00Z"
} }
], ],
"Games": [ "Games": [
@@ -53,6 +63,27 @@
"MaxAnonymousListingsPerAddress": 0, "MaxAnonymousListingsPerAddress": 0,
"MaxActiveJoinAttempts": 100, "MaxActiveJoinAttempts": 100,
"FallbackPolicy": "DedicatedEndpointAllowed" "FallbackPolicy": "DedicatedEndpointAllowed"
},
{
"GameId": "unscouted",
"EnvironmentId": "smoke",
"Enabled": true,
"ProtocolVersions": [1],
"Regions": ["local"],
"VisibilityModes": ["Public"],
"PublisherTrustModes": ["ManagedDedicated"],
"MetadataValueMaxBytes": {
"mode": 32,
"world": 64,
"mods": 64
},
"RequiredMetadataKeys": ["mode", "world", "mods"],
"MetadataMaxBytes": 512,
"MetadataMaxKeys": 3,
"MaxListingsPerPrincipal": 10,
"MaxAnonymousListingsPerAddress": 0,
"MaxActiveJoinAttempts": 100,
"FallbackPolicy": "DedicatedEndpointAllowed"
} }
] ]
} }
+24 -5
View File
@@ -3,9 +3,25 @@ set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
LOCAL_KEY="${RENDEZVOUS_SMOKE_LOCAL_KEY:-$ROOT/deploy/compose/secrets/signing-key}" LOCAL_KEY="${RENDEZVOUS_SMOKE_LOCAL_KEY:-$ROOT/deploy/compose/secrets/signing-key}"
GAME_ID="${RENDEZVOUS_LOCAL_CREDENTIAL_GAME_ID:-space-game}"
case "$GAME_ID" in
space-game)
KEY_ID="local-smoke-1"
SUBJECT="local-smoke-host"
;;
unscouted)
KEY_ID="local-smoke-unscouted-1"
SUBJECT="local-smoke-unscouted-host"
;;
*)
printf 'RENDEZVOUS_LOCAL_CREDENTIAL_GAME_ID must be space-game or unscouted.\n' >&2
exit 2
;;
esac
if (( $# != 0 )); then if (( $# != 0 )); then
printf 'This helper accepts no arguments and mints only the fixed local Compose smoke scope.\n' >&2 printf 'This helper accepts no arguments; select only a provisioned local game through RENDEZVOUS_LOCAL_CREDENTIAL_GAME_ID.\n' >&2
exit 2 exit 2
fi fi
@@ -17,7 +33,7 @@ command -v python3 >/dev/null || {
# This is deliberately a local-fixture tool, not a general credential issuer. # This is deliberately a local-fixture tool, not a general credential issuer.
# Python reads the raw key from the protected file; key material never appears in # Python reads the raw key from the protected file; key material never appears in
# a child process argument, environment value, temporary file, or command output. # a child process argument, environment value, temporary file, or command output.
python3 - "$LOCAL_KEY" <<'PY' python3 - "$LOCAL_KEY" "$GAME_ID" "$KEY_ID" "$SUBJECT" <<'PY'
import base64 import base64
import hashlib import hashlib
import hmac import hmac
@@ -29,6 +45,9 @@ import sys
import time import time
key_path = sys.argv[1] key_path = sys.argv[1]
game_id = sys.argv[2]
key_id = sys.argv[3]
subject = sys.argv[4]
try: try:
metadata = os.lstat(key_path) metadata = os.lstat(key_path)
except FileNotFoundError: except FileNotFoundError:
@@ -55,9 +74,9 @@ payload = {
"version": 1, "version": 1,
"issuer": "final-factory-rendezvous-smoke", "issuer": "final-factory-rendezvous-smoke",
"audience": "rendezvous-service", "audience": "rendezvous-service",
"subject": "local-smoke-host", "subject": subject,
"kind": "dedicatedPublisher", "kind": "dedicatedPublisher",
"gameId": "space-game", "gameId": game_id,
"environmentId": "smoke", "environmentId": "smoke",
"regions": ["local"], "regions": ["local"],
"permissions": [], "permissions": [],
@@ -71,7 +90,7 @@ def base64url(value: bytes) -> str:
return base64.urlsafe_b64encode(value).rstrip(b"=").decode("ascii") return base64.urlsafe_b64encode(value).rstrip(b"=").decode("ascii")
encoded = base64url(json.dumps(payload, separators=(",", ":")).encode("utf-8")) encoded = base64url(json.dumps(payload, separators=(",", ":")).encode("utf-8"))
signed = f"rv1.local-smoke-1.{encoded}" signed = f"rv1.{key_id}.{encoded}"
signature = base64url(hmac.new(key, signed.encode("ascii"), hashlib.sha256).digest()) signature = base64url(hmac.new(key, signed.encode("ascii"), hashlib.sha256).digest())
print(f"{signed}.{signature}") print(f"{signed}.{signature}")
PY PY
@@ -238,6 +238,10 @@ public sealed class ProductionProcessTests
"--Rendezvous:Provisioning:SigningKeys:0:NotBefore", now.AddHours(-1).ToString("O"), "--Rendezvous:Provisioning:SigningKeys:0:NotBefore", now.AddHours(-1).ToString("O"),
"--Rendezvous:Provisioning:SigningKeys:0:SignUntil", now.AddHours(1).ToString("O"), "--Rendezvous:Provisioning:SigningKeys:0:SignUntil", now.AddHours(1).ToString("O"),
"--Rendezvous:Provisioning:SigningKeys:0:VerifyUntil", now.AddHours(2).ToString("O"), "--Rendezvous:Provisioning:SigningKeys:0:VerifyUntil", now.AddHours(2).ToString("O"),
"--Rendezvous:Provisioning:SigningKeys:1:SecretReference", $"file:{secretPath}",
"--Rendezvous:Provisioning:SigningKeys:1:NotBefore", now.AddHours(-1).ToString("O"),
"--Rendezvous:Provisioning:SigningKeys:1:SignUntil", now.AddHours(1).ToString("O"),
"--Rendezvous:Provisioning:SigningKeys:1:VerifyUntil", now.AddHours(2).ToString("O"),
"--Rendezvous:Udp:Port", udpPort.ToString(System.Globalization.CultureInfo.InvariantCulture), "--Rendezvous:Udp:Port", udpPort.ToString(System.Globalization.CultureInfo.InvariantCulture),
"--Rendezvous:Deployment:PublicUdpPort", udpPort.ToString(System.Globalization.CultureInfo.InvariantCulture), "--Rendezvous:Deployment:PublicUdpPort", udpPort.ToString(System.Globalization.CultureInfo.InvariantCulture),
}, },
@@ -40,14 +40,19 @@ public sealed partial class DocumentationContractTests
} }
[Fact] [Fact]
public void LocalCredentialHelperIsFixedScopeAndSmokeDelegatesToIt() public void LocalCredentialHelperWhitelistsProvisionedGameScopesAndSmokeDelegatesToIt()
{ {
string root = FindRepositoryRoot(); string root = FindRepositoryRoot();
string helper = File.ReadAllText(Path.Combine(root, "scripts", "mint-local-publisher-credential.sh")); string helper = File.ReadAllText(Path.Combine(root, "scripts", "mint-local-publisher-credential.sh"));
string smoke = File.ReadAllText(Path.Combine(root, "scripts", "smoke-deployment.sh")); string smoke = File.ReadAllText(Path.Combine(root, "scripts", "smoke-deployment.sh"));
Assert.Contains("if (( $# != 0 ));", helper, StringComparison.Ordinal); Assert.Contains("if (( $# != 0 ));", helper, StringComparison.Ordinal);
Assert.Contains("\"gameId\": \"space-game\"", helper, StringComparison.Ordinal); Assert.Contains("space-game)", helper, StringComparison.Ordinal);
Assert.Contains("unscouted)", helper, StringComparison.Ordinal);
Assert.Contains("KEY_ID=\"local-smoke-1\"", helper, StringComparison.Ordinal);
Assert.Contains("KEY_ID=\"local-smoke-unscouted-1\"", helper, StringComparison.Ordinal);
Assert.Contains("RENDEZVOUS_LOCAL_CREDENTIAL_GAME_ID must be space-game or unscouted", helper, StringComparison.Ordinal);
Assert.Contains("\"gameId\": game_id", helper, StringComparison.Ordinal);
Assert.Contains("\"environmentId\": \"smoke\"", helper, StringComparison.Ordinal); Assert.Contains("\"environmentId\": \"smoke\"", helper, StringComparison.Ordinal);
Assert.Contains("\"regions\": [\"local\"]", helper, StringComparison.Ordinal); Assert.Contains("\"regions\": [\"local\"]", helper, StringComparison.Ordinal);
Assert.Contains("now + 600", helper, StringComparison.Ordinal); Assert.Contains("now + 600", helper, StringComparison.Ordinal);
@@ -60,6 +65,32 @@ public sealed partial class DocumentationContractTests
Assert.DoesNotContain("openssl dgst", smoke, StringComparison.Ordinal); Assert.DoesNotContain("openssl dgst", smoke, StringComparison.Ordinal);
} }
[Fact]
public void UnscoutedComposeTenantIsGameScopedAndMetadataBounded()
{
string root = FindRepositoryRoot();
using JsonDocument settings = JsonDocument.Parse(File.ReadAllText(
Path.Combine(root, "deploy", "compose", "appsettings.Production.json")));
JsonElement provisioning = settings.RootElement.GetProperty("Rendezvous").GetProperty("Provisioning");
JsonElement game = provisioning.GetProperty("Games").EnumerateArray().Single(
static item => item.GetProperty("GameId").GetString() == "unscouted");
JsonElement key = provisioning.GetProperty("SigningKeys").EnumerateArray().Single(
static item => item.GetProperty("KeyId").GetString() == "local-smoke-unscouted-1");
Assert.Equal("smoke", game.GetProperty("EnvironmentId").GetString());
Assert.Equal([1], game.GetProperty("ProtocolVersions").EnumerateArray().Select(static value => value.GetInt32()));
Assert.Equal(["mode", "mods", "world"], game.GetProperty("MetadataValueMaxBytes")
.EnumerateObject().Select(static property => property.Name).Order(StringComparer.Ordinal));
Assert.Equal(["mode", "mods", "world"], game.GetProperty("RequiredMetadataKeys")
.EnumerateArray().Select(static value => value.GetString()).Order(StringComparer.Ordinal));
Assert.Equal(3, game.GetProperty("MetadataMaxKeys").GetInt32());
Assert.Equal("DedicatedEndpointAllowed", game.GetProperty("FallbackPolicy").GetString());
Assert.Equal("unscouted", key.GetProperty("GameId").GetString());
Assert.Equal("smoke", key.GetProperty("EnvironmentId").GetString());
Assert.Equal(["DedicatedPublisher"], key.GetProperty("CredentialKinds")
.EnumerateArray().Select(static value => value.GetString()));
}
[Fact] [Fact]
public void EveryIncidentRunbookHasDetectContainRecoverAndVerifyGates() public void EveryIncidentRunbookHasDetectContainRecoverAndVerifyGates()
{ {