P7 r2 fixes: drift guard wired through pushIfNoDrift (acl_drift_detected ProviderError, never overwrites drifted remote) + unknown-device forward_auth audit + drift regression tests
This commit is contained in:
4 files changed
+84
-3
No files matched your search
@@ -92,7 +92,19 @@ function createAclSync({ coordClient = null, tailnet = '-', pusher = null, compi
|
||||
* @returns {Promise<{pushed: true, hash: string}>}
|
||||
*/
|
||||
async push(policy) {
|
||||
return push.pushPolicy(policy);
|
||||
// Drift guard (spec hard rule): never silently overwrite remote ACL
|
||||
// edits made outside DashCaddy. pushIfNoDrift compares the stored
|
||||
// last-known-good hash against the remote acls hash first; a drifted
|
||||
// remote resolves {drifted:true, pushed:false} which the caller must
|
||||
// surface (provider maps it to ProviderError 'acl_drift_detected').
|
||||
const result = await push.pushIfNoDrift(policy);
|
||||
if (result && result.drifted && !result.pushed) {
|
||||
const err = new Error('remote ACL policy drifted from last-known-good; refusing to overwrite');
|
||||
err.code = 'acl_drift_detected';
|
||||
err.drift = result;
|
||||
throw err;
|
||||
}
|
||||
return result;
|
||||
},
|
||||
|
||||
/** Local last-known-good state (null when never pushed). */
|
||||
|
||||
@@ -188,6 +188,13 @@ function createForwardAuthService({
|
||||
// 1. Identity follows the device: whois the connecting IP.
|
||||
const identity = await whoisIdentity(sourceIp);
|
||||
if (!identity.personId) {
|
||||
audit(auditLog, {
|
||||
event: 'forward_auth_deny',
|
||||
personId: null,
|
||||
resource: null,
|
||||
host: String(host || ''),
|
||||
outcome: 'unknown-device',
|
||||
});
|
||||
return {
|
||||
status: 401,
|
||||
headers: { 'WWW-Authenticate': 'ForwardAuth realm="dashcaddy-private-network"' },
|
||||
|
||||
@@ -417,6 +417,16 @@ function createTailscaleProvider({
|
||||
try {
|
||||
pushed = await engine.push(compiled.policy);
|
||||
} catch (err) {
|
||||
if (err && err.code === 'acl_drift_detected') {
|
||||
// Drift guard (spec hard rule): the remote policy hash no longer
|
||||
// matches our last-known-good — someone edited ACLs outside
|
||||
// DashCaddy. NEVER silently overwrite. Surface as a distinct,
|
||||
// non-retryable ProviderError carrying the drift details.
|
||||
throw new ProviderError(
|
||||
'tailscale: remote ACL policy drifted from DashCaddy last-known-good — refusing to overwrite; review the tailnet ACL console and re-import or force-sync',
|
||||
{ code: 'acl_drift_detected', retryable: false, cause: err, drift: err.drift }
|
||||
);
|
||||
}
|
||||
if (err instanceof AclPushError) {
|
||||
// API-side failure: map the pusher's code through the shared
|
||||
// vocabulary (acl_rejected → bad_input, rate-limited/http_* →
|
||||
|
||||
Reference in new issue
Block a user