Authorize Pacemaker actions with SO_PEERCRED

This commit is contained in:
Lyncoln
2026-09-17 12:45:46 -04:00
parent 773e046d42
commit 5a128fea36
4 changed files with 321 additions and 14 deletions
+120 -4
View File
@@ -4,6 +4,7 @@ import json
import os
import signal
import socket
import struct
import traceback
from registry import Registry, RegistryError
@@ -17,6 +18,8 @@ SOCKET_PATH = os.environ.get(
server_socket = None
registry = None
pacemaker_resource_id = None
pacemaker_peer_uid = None
pacemaker_peer_gid = None
PROTOCOL_VERSION = 1
@@ -65,7 +68,66 @@ def load_pacemaker_resource_binding(
return resource_id
def handle_request(req):
def load_pacemaker_peer_authorization(
environ=None,
):
"""
Load the peer credentials that authorize Pacemaker resource actions.
In the rootless keep-id user namespace, host root is intentionally
unmapped and therefore appears as the configured overflow UID/GID.
Socket path permissions remain part of this authorization boundary.
"""
if environ is None:
environ = os.environ
raw_uid = environ.get("PACEMAKER_PEER_UID")
raw_gid = environ.get("PACEMAKER_PEER_GID")
if raw_uid is None or raw_gid is None:
raise RegistryError(
"PACEMAKER_PEER_UID and "
"PACEMAKER_PEER_GID must both be configured"
)
try:
peer_uid = int(raw_uid)
peer_gid = int(raw_gid)
except ValueError as exc:
raise RegistryError(
"PACEMAKER_PEER_UID and "
"PACEMAKER_PEER_GID must be integers"
) from exc
if peer_uid < 0 or peer_gid < 0:
raise RegistryError(
"PACEMAKER peer credentials must be non-negative"
)
return peer_uid, peer_gid
def get_peer_credentials(conn):
raw = conn.getsockopt(
socket.SOL_SOCKET,
socket.SO_PEERCRED,
struct.calcsize("3i"),
)
pid, uid, gid = struct.unpack("3i", raw)
return {
"pid": pid,
"uid": uid,
"gid": gid,
}
def handle_request(
req,
peer_credentials=None,
):
if not isinstance(req, dict):
return protocol_response({
"ok": False,
@@ -133,6 +195,35 @@ def handle_request(req):
"action": action,
})
if peer_credentials is None:
return protocol_response({
"ok": False,
"error_code": "peer_credentials_unavailable",
"error": (
"peer credentials are required for "
"Pacemaker resource actions"
),
"resource": resource,
"action": action,
})
if (
peer_credentials.get("uid")
!= pacemaker_peer_uid
or peer_credentials.get("gid")
!= pacemaker_peer_gid
):
return protocol_response({
"ok": False,
"error_code": "peer_credential_mismatch",
"error": (
"peer credentials are not authorized "
"for Pacemaker resource actions"
),
"resource": resource,
"action": action,
})
return protocol_response(
registry.execute(
resource,
@@ -170,16 +261,27 @@ def serve():
global server_socket
global registry
global pacemaker_resource_id
global pacemaker_peer_uid
global pacemaker_peer_gid
registry = Registry()
pacemaker_resource_id = (
load_pacemaker_resource_binding(registry)
)
(
pacemaker_peer_uid,
pacemaker_peer_gid,
) = load_pacemaker_peer_authorization()
socket_directory = os.path.dirname(
SOCKET_PATH
)
os.makedirs(
os.path.dirname(SOCKET_PATH),
socket_directory,
exist_ok=True,
)
os.chmod(socket_directory, 0o700)
cleanup_socket()
@@ -192,7 +294,7 @@ def serve():
)
server_socket.bind(SOCKET_PATH)
os.chmod(SOCKET_PATH, 0o660)
os.chmod(SOCKET_PATH, 0o600)
server_socket.listen(16)
print(
@@ -212,12 +314,23 @@ def serve():
flush=True,
)
print(
"Pacemaker peer authorization: "
f"uid={pacemaker_peer_uid} "
f"gid={pacemaker_peer_gid}",
flush=True,
)
try:
while True:
conn, _ = server_socket.accept()
with conn:
try:
peer_credentials = (
get_peer_credentials(conn)
)
raw = b""
while b"\n" not in raw:
@@ -232,7 +345,10 @@ def serve():
raw.decode("utf-8").strip()
)
response = handle_request(req)
response = handle_request(
req,
peer_credentials,
)
except (
RegistryError,