Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0032085139 | |||
| 6570ebb109 | |||
| 204300c927 |
+1
-1
@@ -11,7 +11,7 @@ AGENT_PORT=3100
|
|||||||
AGENT_GATEWAY_TOKEN=
|
AGENT_GATEWAY_TOKEN=
|
||||||
HEADSCALE_BASE_URL=https://atnmy-hs.duckdns.org
|
HEADSCALE_BASE_URL=https://atnmy-hs.duckdns.org
|
||||||
HEADSCALE_API_KEY=
|
HEADSCALE_API_KEY=
|
||||||
HEADSCALE_GATEWAY_USERS=local-platform-gateway,platform-gateway
|
HEADSCALE_GATEWAY_USERS=platform-gateway
|
||||||
HEADSCALE_TEACHING_DEVICE_OWNER=tagged-devices
|
HEADSCALE_TEACHING_DEVICE_OWNER=tagged-devices
|
||||||
LMS_GATEWAY_TOKEN=change-me
|
LMS_GATEWAY_TOKEN=change-me
|
||||||
|
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ steps:
|
|||||||
volumes:
|
volumes:
|
||||||
- /home/future2/Studio_v2/woodpecker/kube/config:/tmp/kubeconfig:ro
|
- /home/future2/Studio_v2/woodpecker/kube/config:/tmp/kubeconfig:ro
|
||||||
commands:
|
commands:
|
||||||
|
- kubectl --kubeconfig=/tmp/kubeconfig apply -f k8s/platform-gateway.yaml
|
||||||
- kubectl --kubeconfig=/tmp/kubeconfig -n studio-system rollout restart deployment/platform-gateway
|
- kubectl --kubeconfig=/tmp/kubeconfig -n studio-system rollout restart deployment/platform-gateway
|
||||||
- kubectl --kubeconfig=/tmp/kubeconfig -n studio-system rollout status deployment/platform-gateway
|
- kubectl --kubeconfig=/tmp/kubeconfig -n studio-system rollout status deployment/platform-gateway
|
||||||
when:
|
when:
|
||||||
|
|||||||
@@ -30,3 +30,16 @@ Default URL: `http://localhost:4500`
|
|||||||
## Network Model
|
## Network Model
|
||||||
|
|
||||||
Students and instructors never connect directly to Headscale/Tailscale device endpoints. Clients call this gateway only. The gateway verifies auth, checks platform grants/session ownership, records audit context, and proxies allowed operations to the mobility agent running on each platform node.
|
Students and instructors never connect directly to Headscale/Tailscale device endpoints. Clients call this gateway only. The gateway verifies auth, checks platform grants/session ownership, records audit context, and proxies allowed operations to the mobility agent running on each platform node.
|
||||||
|
|
||||||
|
## Kubernetes Tailscale Sidecar
|
||||||
|
|
||||||
|
The Kubernetes deployment runs a Tailscale sidecar in the same network namespace as
|
||||||
|
the gateway. Create the one-time Headscale key secret before the first deployment:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl -n studio-system create secret generic platform-gateway-tailscale-auth \
|
||||||
|
--from-literal=authkey='tskey-auth-...'
|
||||||
|
```
|
||||||
|
|
||||||
|
The sidecar persists its machine state in `platform-gateway-tailscale-state`, so
|
||||||
|
subsequent Gateway pod restarts do not consume another pre-auth key.
|
||||||
|
|||||||
@@ -1,3 +1,33 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: ServiceAccount
|
||||||
|
metadata:
|
||||||
|
name: platform-gateway
|
||||||
|
namespace: studio-system
|
||||||
|
---
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: Role
|
||||||
|
metadata:
|
||||||
|
name: platform-gateway-tailscale-state
|
||||||
|
namespace: studio-system
|
||||||
|
rules:
|
||||||
|
- apiGroups: [""]
|
||||||
|
resources: ["secrets"]
|
||||||
|
verbs: ["get", "create", "update", "patch"]
|
||||||
|
---
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: RoleBinding
|
||||||
|
metadata:
|
||||||
|
name: platform-gateway-tailscale-state
|
||||||
|
namespace: studio-system
|
||||||
|
subjects:
|
||||||
|
- kind: ServiceAccount
|
||||||
|
name: platform-gateway
|
||||||
|
namespace: studio-system
|
||||||
|
roleRef:
|
||||||
|
apiGroup: rbac.authorization.k8s.io
|
||||||
|
kind: Role
|
||||||
|
name: platform-gateway-tailscale-state
|
||||||
|
---
|
||||||
apiVersion: apps/v1
|
apiVersion: apps/v1
|
||||||
kind: Deployment
|
kind: Deployment
|
||||||
metadata:
|
metadata:
|
||||||
@@ -15,7 +45,30 @@ spec:
|
|||||||
labels:
|
labels:
|
||||||
app: platform-gateway
|
app: platform-gateway
|
||||||
spec:
|
spec:
|
||||||
|
serviceAccountName: platform-gateway
|
||||||
containers:
|
containers:
|
||||||
|
- name: tailscale
|
||||||
|
image: tailscale/tailscale:stable
|
||||||
|
env:
|
||||||
|
- name: TS_KUBE_SECRET
|
||||||
|
value: platform-gateway-tailscale-state
|
||||||
|
- name: TS_AUTHKEY
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: platform-gateway-tailscale-auth
|
||||||
|
key: authkey
|
||||||
|
- name: TS_HOSTNAME
|
||||||
|
value: platform-gateway
|
||||||
|
- name: TS_USERSPACE
|
||||||
|
value: "false"
|
||||||
|
- name: TS_EXTRA_ARGS
|
||||||
|
value: --login-server=https://atnmy-hs.duckdns.org --accept-routes
|
||||||
|
securityContext:
|
||||||
|
capabilities:
|
||||||
|
add: ["NET_ADMIN"]
|
||||||
|
volumeMounts:
|
||||||
|
- name: dev-net-tun
|
||||||
|
mountPath: /dev/net/tun
|
||||||
- name: platform-gateway
|
- name: platform-gateway
|
||||||
image: localhost:5000/platform-gateway:0.1.0
|
image: localhost:5000/platform-gateway:0.1.0
|
||||||
imagePullPolicy: Always
|
imagePullPolicy: Always
|
||||||
@@ -37,6 +90,11 @@ spec:
|
|||||||
port: 4500
|
port: 4500
|
||||||
initialDelaySeconds: 15
|
initialDelaySeconds: 15
|
||||||
periodSeconds: 20
|
periodSeconds: 20
|
||||||
|
volumes:
|
||||||
|
- name: dev-net-tun
|
||||||
|
hostPath:
|
||||||
|
path: /dev/net/tun
|
||||||
|
type: CharDevice
|
||||||
---
|
---
|
||||||
apiVersion: v1
|
apiVersion: v1
|
||||||
kind: Service
|
kind: Service
|
||||||
|
|||||||
+1
-1
@@ -21,7 +21,7 @@ export const config = {
|
|||||||
headscaleBaseUrl: required('HEADSCALE_BASE_URL', 'http://localhost:8080'),
|
headscaleBaseUrl: required('HEADSCALE_BASE_URL', 'http://localhost:8080'),
|
||||||
headscaleApiKey: process.env.HEADSCALE_API_KEY ?? '',
|
headscaleApiKey: process.env.HEADSCALE_API_KEY ?? '',
|
||||||
headscaleTeachingDeviceOwner: (process.env.HEADSCALE_TEACHING_DEVICE_OWNER ?? 'tagged-devices').trim().replace(/@+$/, ''),
|
headscaleTeachingDeviceOwner: (process.env.HEADSCALE_TEACHING_DEVICE_OWNER ?? 'tagged-devices').trim().replace(/@+$/, ''),
|
||||||
headscaleGatewayUsers: (process.env.HEADSCALE_GATEWAY_USERS ?? process.env.HEADSCALE_GATEWAY_USER ?? 'local-platform-gateway')
|
headscaleGatewayUsers: (process.env.HEADSCALE_GATEWAY_USERS ?? process.env.HEADSCALE_GATEWAY_USER ?? 'platform-gateway')
|
||||||
.split(',')
|
.split(',')
|
||||||
.map((user) => user.trim().replace(/@+$/, ''))
|
.map((user) => user.trim().replace(/@+$/, ''))
|
||||||
.filter(Boolean),
|
.filter(Boolean),
|
||||||
|
|||||||
@@ -27,9 +27,6 @@ export type HeadscalePolicyStatus = {
|
|||||||
raw: HeadscalePolicy | Record<string, unknown> | null
|
raw: HeadscalePolicy | Record<string, unknown> | null
|
||||||
}
|
}
|
||||||
|
|
||||||
const generatedPrefix = 'group:lms-platform-'
|
|
||||||
const gatewayGroup = 'group:lms-platform-gateway'
|
|
||||||
|
|
||||||
export class HeadscaleAdapter {
|
export class HeadscaleAdapter {
|
||||||
async applyGrant(_grant: AccessGrant): Promise<{ applied: boolean; mode: 'noop' | 'headscale' }> {
|
async applyGrant(_grant: AccessGrant): Promise<{ applied: boolean; mode: 'noop' | 'headscale' }> {
|
||||||
return { applied: true, mode: 'noop' }
|
return { applied: true, mode: 'noop' }
|
||||||
@@ -76,53 +73,31 @@ export class HeadscaleAdapter {
|
|||||||
unresolved: string[]
|
unresolved: string[]
|
||||||
error?: string
|
error?: string
|
||||||
}> {
|
}> {
|
||||||
const nodes = await this.listNodes()
|
const gatewaySources = [...new Set(
|
||||||
const unresolved: string[] = []
|
config.headscaleGatewayUsers.map((user) => this.toHeadscaleUserName(user)),
|
||||||
const groups: Record<string, string[]> = {
|
)]
|
||||||
[gatewayGroup]: [...new Set(config.headscaleGatewayUsers.map((user) => this.toHeadscaleUserName(user)))],
|
|
||||||
}
|
|
||||||
const acls: HeadscalePolicy['acls'] = [
|
|
||||||
{
|
|
||||||
// The local bridge initiates discovery from the trusted gateway host.
|
|
||||||
action: 'accept',
|
|
||||||
src: [gatewayGroup],
|
|
||||||
dst: ['tag:teaching-device:*'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// DDS discovery and topic payloads use independent UDP flows, so the
|
|
||||||
// teaching device must also be able to reach the bridge on the gateway.
|
|
||||||
action: 'accept',
|
|
||||||
src: ['tag:teaching-device'],
|
|
||||||
dst: [`${gatewayGroup}:*`],
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
assignments.forEach((assignment, index) => {
|
|
||||||
const users = [...new Set(assignment.studentEmails.map((email) => this.toHeadscaleUserName(email)).filter(Boolean))]
|
|
||||||
if (!users.length) return
|
|
||||||
|
|
||||||
const node = this.findPlatformNode(nodes, assignment)
|
|
||||||
const ip = node ? this.extractIpAddress(node) : null
|
|
||||||
if (!ip) {
|
|
||||||
unresolved.push(assignment.platformName || assignment.agentHostname || assignment.platformId)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const groupName = `${generatedPrefix}${this.toPolicyToken(assignment.platformId || assignment.agentHostname || String(index))}`
|
|
||||||
groups[groupName] = users
|
|
||||||
acls.push({
|
|
||||||
action: 'accept',
|
|
||||||
src: [groupName],
|
|
||||||
dst: [`${ip}:*`],
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
const policy: HeadscalePolicy = {
|
const policy: HeadscalePolicy = {
|
||||||
groups,
|
groups: {},
|
||||||
tagOwners: {
|
tagOwners: {
|
||||||
'tag:teaching-device': [`${config.headscaleTeachingDeviceOwner}@`],
|
'tag:teaching-device': [`${config.headscaleTeachingDeviceOwner}@`],
|
||||||
},
|
},
|
||||||
acls,
|
acls: [
|
||||||
|
{
|
||||||
|
action: 'accept',
|
||||||
|
src: ['*'],
|
||||||
|
dst: ['*:*'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
action: 'accept',
|
||||||
|
src: gatewaySources,
|
||||||
|
dst: ['*:*'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
action: 'accept',
|
||||||
|
src: ['tag:teaching-device'],
|
||||||
|
dst: ['*:*'],
|
||||||
|
},
|
||||||
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await this.applyPolicy(policy)
|
const result = await this.applyPolicy(policy)
|
||||||
@@ -130,8 +105,8 @@ export class HeadscaleAdapter {
|
|||||||
applied: result.applied,
|
applied: result.applied,
|
||||||
mode: 'headscale',
|
mode: 'headscale',
|
||||||
assignmentCount: assignments.length,
|
assignmentCount: assignments.length,
|
||||||
aclCount: acls.length,
|
aclCount: policy.acls.length,
|
||||||
unresolved,
|
unresolved: [],
|
||||||
...(result.error ? { error: result.error } : {}),
|
...(result.error ? { error: result.error } : {}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user