feature/agent #11

Merged
GW_MC merged 10 commits from feature/agent into master 2025-12-22 18:29:29 +08:00
6 changed files with 129 additions and 20 deletions
Showing only changes of commit 61ecd91219 - Show all commits

13
Cargo.lock generated
View File

@@ -2029,6 +2029,18 @@ dependencies = [
"tempfile", "tempfile",
] ]
[[package]]
name = "nix"
version = "0.30.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6"
dependencies = [
"bitflags 2.10.0",
"cfg-if",
"cfg_aliases",
"libc",
]
[[package]] [[package]]
name = "nu-ansi-term" name = "nu-ansi-term"
version = "0.50.3" version = "0.50.3"
@@ -4824,6 +4836,7 @@ version = "0.1.0"
dependencies = [ dependencies = [
"axum", "axum",
"clap", "clap",
"nix",
"serde", "serde",
"serde_json", "serde_json",
"tokio", "tokio",

View File

@@ -12,3 +12,4 @@ serde_json = { version = "1.0.145", features = ["std"] }
serde = { version = "1.0.228", features = ["std", "derive"] } serde = { version = "1.0.228", features = ["std", "derive"] }
tokio-cron-scheduler = { version = "0.15.1", features = ["signal"] } tokio-cron-scheduler = { version = "0.15.1", features = ["signal"] }
clap = { version = "4", features = ["derive"] } clap = { version = "4", features = ["derive"] }
nix = { version = "0.30.1", features = ["user", "fs"] }

View File

@@ -23,6 +23,9 @@ ENV S6_KEEP_ENV=1
ENV YANPM_AGENT_SOCK=/var/run/yanpm/yanpm-agent.sock ENV YANPM_AGENT_SOCK=/var/run/yanpm/yanpm-agent.sock
ENV YANPM_NGINX_CONFIG_DIR=/etc/nginx/conf.d ENV YANPM_NGINX_CONFIG_DIR=/etc/nginx/conf.d
ENV YANPM_AGENT_SOCK_PERM=660 ENV YANPM_AGENT_SOCK_PERM=660
ENV YANPM_AGENT_SOCK_GID=""
ENV YANPM_AGENT_UID=1000
ENV YANPM_AGENT_GID=1000
WORKDIR /app WORKDIR /app
@@ -36,23 +39,18 @@ RUN tar -C / -Jxpf /tmp/s6-overlay-noarch.tar.xz && rm /tmp/s6-overlay-noarch.ta
ADD https://github.com/just-containers/s6-overlay/releases/download/${S6_OVERLAY_VERSION}/s6-overlay-x86_64.tar.xz /tmp/s6-overlay.tar.xz ADD https://github.com/just-containers/s6-overlay/releases/download/${S6_OVERLAY_VERSION}/s6-overlay-x86_64.tar.xz /tmp/s6-overlay.tar.xz
RUN tar -C / -Jxpf /tmp/s6-overlay.tar.xz && rm /tmp/s6-overlay.tar.xz RUN tar -C / -Jxpf /tmp/s6-overlay.tar.xz && rm /tmp/s6-overlay.tar.xz
# Create non-root user for agent and set permissions # Runtime user creation handled by s6 cont-init (see /etc/cont-init.d)
RUN addgroup -S app && adduser -S -G app app # create directory for yanpm agent socket; ownership will be fixed at container start
RUN mkdir -p /var/run/yanpm
# add user to nginx group to allow reading of nginx configs
RUN adduser app nginx
# create directory for yanpm agent socket
RUN mkdir -p /var/run/yanpm && chown -R app:app /var/run/yanpm
# Copy s6 service definitions (created in repo under s6/) into image # Copy s6 service definitions (created in repo under s6/) into image
COPY ./docker/s6/services.d /etc/services.d COPY ./docker/s6/services.d /etc/services.d
RUN chmod +x /etc/services.d/*/run COPY ./docker/s6/cont-init.d /etc/cont-init.d
RUN chmod +x /etc/services.d/*/run && chmod +x /etc/cont-init.d/*
COPY --from=builder /app/target/release/yanpm-agent ./yanpm-agent COPY --from=builder /app/target/release/yanpm-agent ./yanpm-agent
RUN chown -R app:app /app/yanpm-agent \ RUN chmod +x /app/yanpm-agent
&& chmod +x /app/yanpm-agent \
&& chown app:app /app
# s6-overlay provides /init as the init process # s6-overlay provides /init as the init process
ENTRYPOINT ["/init"] ENTRYPOINT ["/init"]

View File

@@ -0,0 +1,58 @@
#!/bin/sh
set -eu
YANPM_AGENT_UID="${YANPM_AGENT_UID:-1000}"
YANPM_AGENT_GID="${YANPM_AGENT_GID:-1000}"
# If a specific socket GID is requested, prefer that for the app group
YANPM_AGENT_GID_EFFECTIVE="${YANPM_AGENT_SOCK_GID:-${YANPM_AGENT_GID}}"
YANPM_AGENT_USER="${YANPM_AGENT_USER:-yanpm-agent}"
YANPM_AGENT_GROUP="${YANPM_AGENT_GROUP:-yanpm-agent}"
# Ensure group exists with desired GID
if grep -qE "^${YANPM_AGENT_GROUP}:" /etc/group 2>/dev/null; then
existing_gid=$(awk -F: -v g="${YANPM_AGENT_GROUP}" '$1==g{print $3}' /etc/group)
if [ "${existing_gid}" != "${YANPM_AGENT_GID_EFFECTIVE}" ]; then
delgroup "${YANPM_AGENT_GROUP}" || true
addgroup -g "${YANPM_AGENT_GID_EFFECTIVE}" "${YANPM_AGENT_GROUP}"
fi
else
addgroup -g "${YANPM_AGENT_GID_EFFECTIVE}" "${YANPM_AGENT_GROUP}"
fi
# Ensure user exists with desired UID and primary group
if grep -qE "^${YANPM_AGENT_USER}:" /etc/passwd 2>/dev/null; then
existing_uid=$(awk -F: -v u="${YANPM_AGENT_USER}" '$1==u{print $3}' /etc/passwd)
if [ "${existing_uid}" != "${YANPM_AGENT_UID}" ]; then
deluser "${YANPM_AGENT_USER}" || true
adduser -D -u "${YANPM_AGENT_UID}" -G "${YANPM_AGENT_GROUP}" "${YANPM_AGENT_USER}"
fi
else
adduser -D -u "${YANPM_AGENT_UID}" -G "${YANPM_AGENT_GROUP}" "${YANPM_AGENT_USER}"
fi
# Add app user to nginx group to allow reading configs
addgroup "${YANPM_AGENT_USER}" nginx || true
# Ensure runtime directories exist and fix ownership
mkdir -p /var/run/yanpm /app
if chown -R "${YANPM_AGENT_UID}:${YANPM_AGENT_GID_EFFECTIVE}" /var/run/yanpm 2>/dev/null; then
echo "chown: /var/run/yanpm -> ${YANPM_AGENT_UID}:${YANPM_AGENT_GID_EFFECTIVE}"
else
echo "Warning: failed to chown /var/run/yanpm to ${YANPM_AGENT_UID}:${YANPM_AGENT_GID_EFFECTIVE}. This is common for bind-mounted host volumes or rootless Docker." >&2
fi
if chown -R "${YANPM_AGENT_UID}:${YANPM_AGENT_GID_EFFECTIVE}" /app/yanpm-agent 2>/dev/null; then
echo "chown: /app/yanpm-agent -> ${YANPM_AGENT_UID}:${YANPM_AGENT_GID_EFFECTIVE}"
else
echo "Warning: failed to chown /app/yanpm-agent to ${YANPM_AGENT_UID}:${YANPM_AGENT_GID_EFFECTIVE}. Binary will still be used if permissions allow." >&2
fi
if chown "${YANPM_AGENT_UID}:${YANPM_AGENT_GID_EFFECTIVE}" /app 2>/dev/null; then
echo "chown: /app -> ${YANPM_AGENT_UID}:${YANPM_AGENT_GID_EFFECTIVE}"
else
echo "Warning: failed to chown /app to ${YANPM_AGENT_UID}:${YANPM_AGENT_GID_EFFECTIVE}." >&2
fi
echo "App user and group setup complete. UID:${YANPM_AGENT_UID} GID:${YANPM_AGENT_GID_EFFECTIVE}"
exit 0

View File

@@ -1,5 +1,5 @@
#!/bin/sh #!/bin/sh
# Run the agent as the unprivileged 'app' user # Run the agent as the unprivileged 'yanpm-agent' user
cd /app cd /app
echo "Starting yanpm-agent..." echo "Starting yanpm-agent..."
exec s6-setuidgid app ./yanpm-agent exec s6-setuidgid yanpm-agent ./yanpm-agent

View File

@@ -18,12 +18,15 @@ use crate::routes::{status, validate, validate_and_reload, write_config};
const SOCK_ARG: &str = "sock"; const SOCK_ARG: &str = "sock";
const NGINX_CONFIG_DIR_ARG: &str = "nginx_config_dir"; const NGINX_CONFIG_DIR_ARG: &str = "nginx_config_dir";
const SOCK_PERM_ARG: &str = "sock_perm"; const SOCK_PERM_ARG: &str = "sock_perm";
const SOCK_GID_ARG: &str = "sock_gid";
const SOCK_ENV: &str = "YANPM_AGENT_SOCK"; const SOCK_ENV: &str = "YANPM_AGENT_SOCK";
const SOCK_PERM_ENV: &str = "YANPM_AGENT_SOCK_PERM"; const SOCK_PERM_ENV: &str = "YANPM_AGENT_SOCK_PERM";
const NGINX_CONFIG_DIR_ENV: &str = "YANPM_NGINX_CONFIG_DIR"; const NGINX_CONFIG_DIR_ENV: &str = "YANPM_NGINX_CONFIG_DIR";
const SOCK_GID_ENV: &str = "YANPM_AGENT_SOCK_GID";
const SOCK_DEFAULT: &str = "./yanpm-agent.sock"; const SOCK_DEFAULT: &str = "./yanpm-agent.sock";
const NGINX_CONFIG_DIR_DEFAULT: &str = "/etc/nginx/conf.d"; const NGINX_CONFIG_DIR_DEFAULT: &str = "/etc/nginx/conf.d";
const SOCK_PERM_DEFAULT: &str = "660"; const SOCK_PERM_DEFAULT: &str = "660";
const SOCK_GID_DEFAULT: &str = "";
#[tokio::main] #[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> { async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
@@ -39,7 +42,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let args = Command::new("yanpm-agent") let args = Command::new("yanpm-agent")
.arg( .arg(
Arg::new("sock") Arg::new(SOCK_ARG)
.short('s') .short('s')
.long("sock") .long("sock")
.value_name("SOCK_PATH") .value_name("SOCK_PATH")
@@ -47,7 +50,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
.required(false), .required(false),
) )
.arg( .arg(
Arg::new("nginx_config_dir") Arg::new(NGINX_CONFIG_DIR_ARG)
.short('d') .short('d')
.long("nginx-config-dir") .long("nginx-config-dir")
.value_name("NGINX_CONFIG_DIR") .value_name("NGINX_CONFIG_DIR")
@@ -55,16 +58,23 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
.required(false), .required(false),
) )
.arg( .arg(
Arg::new("sock_perm") Arg::new(SOCK_PERM_ARG)
.long("sock-perm") .long("sock-perm")
.value_name("SOCK_PERM") .value_name("SOCK_PERM")
.help("Permissions to set on the unix socket (in octal), e.g. 660") .help("Permissions to set on the unix socket (in octal), e.g. 660")
.required(false), .required(false),
) )
.arg(
Arg::new(SOCK_GID_ARG)
.long("sock-gid")
.value_name("SOCK_GID")
.help("GID to set on the unix socket, default: current user's primary group")
.required(false),
)
.about("YANPM Agent Daemon") .about("YANPM Agent Daemon")
.get_matches(); .get_matches();
let (sock, nginx_config_dir, sock_perm) = get_args(&args).await?; let (sock, nginx_config_dir, sock_perm, sock_gid) = get_args(&args).await?;
let path = PathBuf::from(&sock); let path = PathBuf::from(&sock);
if let Some(dir) = path.parent() { if let Some(dir) = path.parent() {
@@ -123,6 +133,27 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
); );
} }
// set socket gid to sock_gid (best-effort)
if !sock_gid.is_empty() {
use nix::unistd::{Gid, chown};
if let Err(err) = chown(
&path,
None,
Some(Gid::from_raw(
sock_gid
.parse()
.map_err(|e| format!("Failed to parse socket GID {}: {}", sock_gid, e))
.unwrap_or_else(|_| nix::unistd::getgid().as_raw()),
)),
) {
error!(
"Warning: failed to set GID on socket {}: {}",
path.display(),
err
);
}
}
let scheduler = Arc::new(tokio_cron_scheduler::JobScheduler::new().await?); let scheduler = Arc::new(tokio_cron_scheduler::JobScheduler::new().await?);
let app = Router::new() let app = Router::new()
@@ -145,7 +176,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
async fn get_args( async fn get_args(
args: &clap::ArgMatches, args: &clap::ArgMatches,
) -> Result<(String, String, u32), Box<dyn std::error::Error + Send + Sync>> { ) -> Result<(String, String, u32, String), Box<dyn std::error::Error + Send + Sync>> {
let sock = args let sock = args
.get_one::<String>(SOCK_ARG) .get_one::<String>(SOCK_ARG)
.cloned() .cloned()
@@ -164,6 +195,13 @@ async fn get_args(
std::env::var(SOCK_PERM_ENV).unwrap_or_else(|_| SOCK_PERM_DEFAULT.to_string()) std::env::var(SOCK_PERM_ENV).unwrap_or_else(|_| SOCK_PERM_DEFAULT.to_string())
}); });
let sock_gid = args
.get_one::<String>(SOCK_GID_ARG)
.cloned()
.unwrap_or_else(|| {
std::env::var(SOCK_GID_ENV).unwrap_or_else(|_| SOCK_GID_DEFAULT.to_string())
});
if sock_perm.len() != 3 || !sock_perm.chars().all(|c| ('0'..='7').contains(&c)) { if sock_perm.len() != 3 || !sock_perm.chars().all(|c| ('0'..='7').contains(&c)) {
return Err(std::io::Error::new( return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput, std::io::ErrorKind::InvalidInput,
@@ -177,8 +215,8 @@ async fn get_args(
if sock_perm.chars().last().unwrap() > '0' { if sock_perm.chars().last().unwrap() > '0' {
warn!( warn!(
"Socket permission string {} allows others to access the socket. This may be a security risk.", "Socket permission string {} allows others to access the socket. This may be a security risk. Consider setting {} to a desired group and using a socket permission string that does not allow others to access the socket.",
sock_perm sock_perm, SOCK_GID_ENV
); );
}; };
@@ -186,5 +224,6 @@ async fn get_args(
sock, sock,
nginx_config_dir, nginx_config_dir,
u32::from_str_radix(&sock_perm, 8).expect("Failed to parse socket permission string"), u32::from_str_radix(&sock_perm, 8).expect("Failed to parse socket permission string"),
sock_gid,
)) ))
} }