Page Menu
Home
VyOS Platform
Search
Configure Global Search
Log In
Files
F116353134
T9030-mcp-security.md
All Users
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Authored By
mihakralj
Jul 1 2026, 2:50 AM
2026-07-01 02:50:46 (UTC+0)
Size
10 KB
Referenced Files
None
Subscribers
None
T9030-mcp-security.md
View Options
#
T9030
:
Security
,
CLI
Integration
&
Concurrency
|
Field
|
Value
|
|-------|-------|
|
Task
|
[
T9030
](
https
:
//vyos.dev/T9030) |
|
Status
|
Draft
/
design
|
|
Related
specs
|
[
Architecture
&
Transport
](
T9030
-
mcp
-
architecture
.
md
)
·
[
Dynamic
Schema
](
T9030
-
mcp
-
dynamic
-
schema
.
md
)
·
[
MCP
Tools
](
T9030
-
mcp
-
tools
.
md
)
|
##
Authentication
The
MCP
endpoint
inherits
the
existing
VyOS
HTTPS
API
authentication
layer
.
No
separate
OAuth
2.1
flow
is
required
.
###
Mechanism
Credentials
are
validated
at
the
`
GET
/
mcp
`
handshake
**
before
**
any
MCP
logic
executes
.
The
HTTP
API
server
already
loads
the
configured
keys
from
`
api_config_state
`
(
`
/
run
/
http
-
api
-
state
`
)
and
flattens
them
into
`
{
id
,
key
}
`
records
via
`
flatten_keys
()
`
;
MCP
reuses
that
key
list
and
the
`
check_auth
()
`
matcher
to
resolve
a
request
to
a
`
key_id
`
.
>
**
Note
—
extraction
differs
from
REST
/
GraphQL
.**
The
existing
REST
and
GraphQL
>
endpoints
read
the
API
key
from
the
**
request
body
**
(
`
auth_required
(
data
:
>
ApiModel
)
`
consults
`
data
.
key
`
).
A
`
GET
`
handshake
has
no
body
,
so
MCP
>
must
extract
the
credential
from
the
request
itself
:
>
>
-
`
apikey
`
HTTP
header
(
or
,
where
the
client
cannot
set
custom
headers
,
a
>
query
parameter
)
→
matched
against
configured
API
keys
via
`
check_auth
()
`
.
>
-
`
Authorization
:
Bearer
<
token
>
`
→
JWT
validated
with
`
HS256
`
against
the
>
generated
shared
secret
(
reusing
`
api
/
graphql
/
libs
/
token_auth
.
py
`
;
handles
>
`
ExpiredSignatureError
`
/
`
PyJWTError
`
).
>
>
Only
the
**
extraction
surface
**
is
new
;
the
key
-
matching
and
JWT
-
verification
>
logic
is
shared
with
the
existing
endpoints
.
This
new
surface
lives
in
>
`
api
/
mcp
/
auth
.
py
`
and
must
be
covered
by
tests
.
On
failure
,
HTTP
401
is
returned
immediately
at
the
`
GET
/
mcp
`
handshake
—
no
MCP
handshake
,
no
tool
enumeration
,
no
resource
access
,
and
the
SSE
stream
is
never
opened
.
###
Resolved
identity
A
successful
auth
sets
the
session
identity
(
`
key_id
`
for
key
auth
,
or
the
JWT
`
user
`
for
token
auth
).
This
identity
is
attached
to
every
subsequent
`
tools
/
call
`
on
the
session
and
is
used
for
audit
logging
(
see
below
).
###
Shared
Threat
Model
Identical
cryptographic
boundary
as
the
REST
and
GraphQL
endpoints
.
IP
filtering
(
`
set
service
https
allow
-
client
address
`
)
applies
uniformly
because
MCP
is
mounted
on
the
same
FastAPI
app
behind
the
same
TLS
termination
and
unix
socket
(
`
/
run
/
api
.
sock
`
).
|
Threat
|
Mitigation
|
|--------|------------|
|
Credential
theft
|
Same
key
/
JWT
model
as
REST
;
TLS
-
only
;
IP
allow
-
list
.
|
|
Command
injection
/
hallucinated
CLI
|
Every
path
validated
against
`
vyos
.
xml_ref
`
;
no
shell
string
is
ever
built
from
LLM
input
.
|
|
Privilege
escalation
via
writes
|
`
read
-
only
`
mode
omits
all
write
tools
from
the
catalog
(
structural
,
not
advisory
).
|
|
Resource
path
traversal
|
`
{
path
}
`
sanitized
(
reject
`
..
`
and
shell
metacharacters
)
before
backend
dispatch
.
|
|
Denial
of
service
via
long
commits
|
Blocking
work
offloaded
to
threads
;
event
loop
and
keep
-
alives
stay
responsive
.
|
|
Config
corruption
via
concurrent
commits
|
All
writes
serialized
through
the
shared
REST
`
lock
`
/
`
asynclock
`
;
MCP
and
REST
commits
are
mutually
exclusive
.
|
|
Information
disclosure
|
Schema
resources
exposed
only
when
the
`
introspection
`
flag
is
set
(
off
by
default
).
|
##
CLI
Configuration
Commands
nested
within
`
service
https
api
mcp
`
:
```
set
service
https
api
mcp
#
Enable
MCP
endpoint
(
default
:
disabled
)
set
service
https
api
mcp
mode
<
read
-
only
|
read
-
write
>
#
Tool
access
level
(
default
:
read
-
only
)
set
service
https
api
mcp
introspection
#
Expose
schema
resources
(
valueless
flag
)
```
>
A
dedicated
MCP
listen
port
is
intentionally
omitted
from
the
initial
CLI
:
MCP
>
shares
the
single
Uvicorn
server
bound
to
`
/
run
/
api
.
sock
`
and
is
reverse
-
proxied
>
on
the
existing
HTTPS
port
.
Shipping
a
`
port
`
node
that
the
daemon
ignores
would
>
be
misleading
;
port
decoupling
is
tracked
as
future
work
(
see
the
>
[
Architecture
open
questions
](
T9030
-
mcp
-
architecture
.
md
#
open
-
questions
)).
###
Node
semantics
|
Path
|
Type
|
Default
|
Effect
|
|------|------|---------|--------|
|
`
mcp
`
|
node
(
presence
)
|
absent
→
disabled
|
Enables
MCP
routing
when
present
.
|
|
`
mcp
/
mode
`
|
enum
`
read
-
only
`
/
`
read
-
write
`
|
`
read
-
only
`
|
Controls
which
tools
are
registered
.
|
|
`
mcp
/
introspection
`
|
valueless
flag
|
absent
|
When
set
,
schema
resources
are
exposed
.
|
`
read
-
only
`
is
the
secure
default
:
even
if
credentials
leak
,
an
attacker
cannot
mutate
configuration
or
reboot
the
device
through
MCP
.
###
Reconfiguration
Behavior
On
commit
:
1
.
`
service_https
.
py
`
rebuilds
the
API
state
via
`
get_config_dict
()
`
and
rewrites
`
/
run
/
http
-
api
-
state
`
.
2
.
The
running
Uvicorn
server
reconciles
routers
(
no
process
restart
).
3
.
`
api
.
mcp
.
routers
.
mcp_clear
(
app
)
`
cancels
the
session
manager
lifespan
,
terminating
all
active
sessions
.
4
.
`
mcp_init
(
app
)
`
re
-
mounts
the
transport
with
the
new
mode
/
introspection
constraints
(
or
stays
cleared
if
`
mcp
`
was
removed
).
5
.
REST
/
GraphQL
connections
are
unaffected
.
##
XML
Interface
Definitions
New
CLI
nodes
are
added
under
the
existing
`
service
https
api
`
subtree
in
`
interface
-
definitions
/
service_https
.
xml
.
in
`
,
reusing
the
conventions
of
the
sibling
`
rest
`
/
`
graphql
`
nodes
:
a
valueless
flag
for
the
boolean
toggle
and
an
**
unanchored
**
`
<
regex
>
`
constraint
(
VyOS
anchors
constraint
regexes
implicitly
;
do
not
wrap
them
in
`
^...$
`
).
Sketch
:
```
xml
<
node
name
=
"mcp"
>
<
properties
>
<
help
>
Model
Context
Protocol
(
MCP
)
API
</
help
>
</
properties
>
<
children
>
<
leafNode
name
=
"mode"
>
<
properties
>
<
help
>
MCP
tool
access
level
</
help
>
<
completionHelp
>
<
list
>
read
-
only
read
-
write
</
list
>
</
completionHelp
>
<
constraint
>
<
regex
>(
read
-
only
|
read
-
write
)</
regex
>
</
constraint
>
</
properties
>
<
defaultValue
>
read
-
only
</
defaultValue
>
</
leafNode
>
<
leafNode
name
=
"introspection"
>
<
properties
>
<
help
>
Expose
schema
introspection
resources
</
help
>
<
valueless
/>
</
properties
>
</
leafNode
>
</
children
>
</
node
>
```
These
nodes
render
`
api_config_state
`
entries
consumed
by
`
vyos
-
http
-
api
-
server
`
,
where
they
are
read
as
`
session
.
mcp
`
,
`
session
.
mcp_mode
`
,
and
`
session
.
mcp_introspection
`
.
##
Audit
Logging
-
Each
`
modify_configuration
`
invocation
runs
under
a
`
ConfigSession
(
uid
,
app
=
"vyos-mcp-agent"
)
`
;
the
`
app
`
label
tags
the
commit
in
the
commit
log
so
MCP
-
originated
changes
are
attributable
.
-
The
resolved
auth
identity
(
`
key_id
`
/
JWT
user
)
plus
the
session
UID
(
derived
from
the
ASGI
worker
PID
)
are
logged
with
each
tool
call
.
-
Failed
validations
(
invalid
paths
,
rejected
commands
)
are
logged
at
the
transport
boundary
before
any
backend
dispatch
.
###
Observability
and
Metrics
-
MCP
-
specific
metrics
will
be
exposed
if
`
service
monitoring
prometheus
`
is
enabled
.
-
Metrics
include
:
-
Active
Streamable
HTTP
sessions
-
Tool
invocation
counts
(
categorized
by
success
/
failure
and
tool
type
)
-
Latency
of
tool
execution
(
to
monitor
for
slow
configurations
impacting
the
event
loop
)
##
Concurrency
&
Event
Loop
###
Problem
`
ConfigSession
.
commit
()
`
executes
**
synchronous
**
subprocess
calls
(
the
CLI
shell
API
,
iproute2
,
Jinja2
template
rendering
,
systemd
daemon
reloads
).
If
run
directly
in
the
FastAPI
event
loop
,
the
entire
ASGI
server
stalls
:
session
keep
-
alive
pings
stop
and
the
LLM
connection
times
out
.
###
Solution
All
operational
and
configuration
tool
bodies
are
wrapped
in
`
asyncio
.
to_thread
()
`
(
or
Starlette
'
s
`
run_in_threadpool
()
`
):
1
.
The
FastAPI
/
MCP
handler
receives
the
tool
invocation
on
the
event
loop
.
2
.
It
immediately
offloads
`
ConfigSession
`
instantiation
,
the
`
set
`
/
`
delete
`
mapping
,
and
`
commit
()
`
to
a
worker
thread
.
3
.
The
primary
Uvicorn
event
loop
continues
processing
`
POST
/
mcp
/
messages
`
and
transmitting
keep
-
alive
frames
.
4
.
The
thread
result
(
success
/
error
)
is
returned
to
the
event
loop
for
JSON
-
RPC
serialization
over
the
Streamable
HTTP
response
.
Resource
reads
(
config
/
state
/
schema
)
are
likewise
offloaded
when
they
shell
out
to
op
-
mode
scripts
;
pure
in
-
memory
schema
projection
stays
on
the
loop
.
###
Serializing
commits
Offloading
to
a
worker
thread
keeps
the
event
loop
responsive
but
does
**
not
**
make
concurrent
configuration
changes
safe
.
`
ConfigSession
.
commit
()
`
mutates
a
shared
candidate
/
running
configuration
;
two
overlapping
`
modify_configuration
`
calls
(
or
an
MCP
commit
racing
a
REST
`
POST
/
configure
`
)
would
corrupt
that
shared
state
.
The
REST
router
already
guards
this
with
a
module
-
level
`
threading
.
Lock
`
(
`
lock
`
)
and
an
`
asyncio
.
Lock
`
(
`
asynclock
`
)
in
`
src
/
services
/
api
/
rest
/
routers
.
py
`
.
MCP
**
must
**
serialize
writes
through
the
*
same
*
locks
,
not
new
ones
,
so
that
MCP
-
originated
and
REST
-
originated
commits
are
mutually
exclusive
across
the
whole
API
process
:
1
.
Acquire
`
asynclock
`
on
the
event
loop
.
2
.
Offload
the
`
ConfigSession
`
staging
+
`
commit
()
`
to
a
worker
thread
that
holds
the
blocking
`
lock
`
for
the
duration
of
the
commit
.
3
.
Release
both
once
the
commit
(
or
error
)
returns
.
Read
-
only
operational
commands
and
resource
reads
do
not
take
the
write
lock
and
remain
concurrent
.
###
Error
Serialization
-
The
MCP
tool
dispatcher
wraps
every
tool
body
in
a
`
try
/
except
`
that
catches
`
ConfigError
`
,
`
ConfigSessionError
`
,
and
the
`
vyos
.
opmode
`
error
classes
and
converts
them
into
JSON
-
RPC
`
error
`
objects
(
rather
than
letting
a
bare
exception
escape
as
a
generic
transport
error
).
-
The
exact
semantic
error
string
(
e
.
g
.,
"Cannot assign IPv6 address to bridge
interface without enabling IPv6 forwarding"
)
is
serialized
into
the
JSON
-
RPC
error
`
message
`
/
`
data
`
.
-
The
LLM
receives
the
precise
failure
rationale
,
enabling
self
-
correction
(
it
re
-
reads
the
schema
resource
and
reissues
a
corrected
`
modify_configuration
`
).
##
Process
Model
-
Single
Uvicorn
worker
process
(
consistent
with
the
current
VyOS
API
),
binding
the
existing
unix
socket
`
/
run
/
api
.
sock
`
.
-
Each
MCP
connection
is
handled
as
an
ASGI
scope
.
-
`
ConfigSession
`
instances
are
ephemeral
—
created
per
`
modify_configuration
`
invocation
and
discarded
after
commit
/
error
(
no
long
-
lived
staged
state
shared
between
unrelated
tool
calls
).
-
The
session
UID
is
derived
from
the
ASGI
worker
PID
for
audit
logging
,
matching
how
`
vyos
-
http
-
api
-
server
`
seeds
`
ConfigSession
(
os
.
getpid
())
`
.
File Metadata
Details
Attached
Mime Type
text/plain
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
3820424
Default Alt Text
T9030-mcp-security.md (10 KB)
Attached To
Mode
T9030: Native HTTPS Model Context Protocol (MCP) API Endpoint
Attached
Detach File
Event Timeline
Log In to Comment