Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions charmcraft.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,20 @@ config:
description: |
Comma-separated list of labels to be assigned to the agent in Jenkins. If not set it will
default to the agents hardware identifier, e.g.: 'x86_64'
agent_user:
type: string
default: jenkins
description: |
OS user that runs the Jenkins agent systemd service. Defaults to jenkins.
The charm ensures the user exists, owns JENKINS_HOME, and is granted
passwordless sudo. Note that the systemd unit file itself is always owned
by root.
jenkins_home:
type: string
default: /var/lib/jenkins
description: |
Home directory for the Jenkins agent, passed to the agent as -workDir and
used as the working directory by the systemd service.
websocket_mode:
type: boolean
default: true
Expand Down
12 changes: 12 additions & 0 deletions docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,18 @@

Each revision is versioned by the date of the revision.

## August 12, 2026

Check warning on line 9 in docs/changelog.md

View workflow job for this annotation

GitHub Actions / docs-checks / vale

[vale] reported by reviewdog 🐶 [Canonical.008-Headings-no-punctuation] Don't use punctuation in headings. Raw Output: {"message":"[Canonical.008-Headings-no-punctuation] Don't use punctuation in headings.","location":{"path":"docs/changelog.md","range":{"start":{"line":9,"column":13},"end":{"line":9,"column":15}}},"severity":"INFO","code":{"value":"Canonical.008-Headings-no-punctuation","url":"https://canonical-documentation-style-guide.readthedocs-hosted.com/"}}

- Harden configurable agent user and home directory handling with validated paths,
least-privilege ownership, and fail-fast account setup.
- Keep password-free privileged access provisioning deterministic and fail closed
when validation or installation fails.
- Render systemd environment values with systemd escaping instead of HTML
escaping, reject line-breaking control characters, and avoid logging secrets.
- Fail installation when the agent account, home directory, package setup, or
privileged-rule validation cannot be completed; only the configured home
directory itself is re-owned.

## 2026-07-06

- Add `-websocket` flag to agent connection to support HTTP-only reverse proxies (traefik-k8s ingress).
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ integration = [
"protobuf==7.35.1",
"pytest",
"pytest-operator",
"tenacity",
]

[tool.uv]
Expand Down
21 changes: 21 additions & 0 deletions src/charm_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@

import logging
import os
import re
import typing
from dataclasses import dataclass
from pathlib import Path

import ops
from dotenv import dotenv_values
Expand All @@ -17,6 +19,8 @@
AGENT_RELATION = "agent"

logger = logging.getLogger()
_AGENT_USER_PATTERN = re.compile(r"^[a-z_][a-z0-9_-]{0,31}\$?$")
_JENKINS_HOME_PATTERN = re.compile(r"^/[A-Za-z0-9._/-]+$")


class Credentials(BaseModel):
Expand Down Expand Up @@ -132,6 +136,8 @@ class State:
unit_data: UnitData
websocket_mode: bool
jenkins_agent_service_name: str = "jenkins-agent"
agent_user: str = "jenkins"
jenkins_home: Path = Path("/var/lib/jenkins")

@classmethod
def from_charm(cls, charm: ops.CharmBase) -> "State":
Expand Down Expand Up @@ -177,9 +183,24 @@ def from_charm(cls, charm: ops.CharmBase) -> "State":
# Get websocket_mode config
websocket_mode = bool(charm.model.config.get("websocket_mode", True))

# Get user/home config
agent_user = str(charm.model.config.get("agent_user", "jenkins") or "jenkins")
jenkins_home = Path(
str(charm.model.config.get("jenkins_home", "/var/lib/jenkins") or "/var/lib/jenkins")
)
if (
not _AGENT_USER_PATTERN.fullmatch(agent_user)
or not _JENKINS_HOME_PATTERN.fullmatch(str(jenkins_home))
or jenkins_home == Path("/")
or ".." in jenkins_home.parts
):
raise InvalidStateError("Invalid agent configuration.")

return cls(
agent_meta=agent_meta,
agent_relation_credentials=agent_relation_credentials,
unit_data=unit_data,
websocket_mode=websocket_mode,
agent_user=agent_user,
jenkins_home=jenkins_home,
)
Loading
Loading