Skip to content

Fix(agent): Improve tunnel preservation during agent upgrade when using tunnel. Housekeeping of shellhub-agent files in own directory - #5943

Open
ltan10 wants to merge 2 commits into
shellhub-io:masterfrom
ltan10:chore/group-agent-etc-files
Open

Fix(agent): Improve tunnel preservation during agent upgrade when using tunnel. Housekeeping of shellhub-agent files in own directory#5943
ltan10 wants to merge 2 commits into
shellhub-io:masterfrom
ltan10:chore/group-agent-etc-files

Conversation

@ltan10

@ltan10 ltan10 commented Mar 4, 2026

Copy link
Copy Markdown

What kind of change does this PR introduce?

  • Bugfix
  • New Feature
  • Feature Improvment
  • Refactoring
  • Documentation
  • Other, please describe:

Description:

  • Organized go based agent config and key files into own dedicated directory in /etc/shellhub-agent/
    • Does not remove existing /etc/shellhub.key and /etc/shellhub-agent.env or /opt/shellhub/
    • Best migration procedure would be to copy shellhub.key to /etc/shellhub-agent/shellhub.key prior to performing installation/upgrade
  • Modified default PRIVATE_KEY location value for podman, snap and docker installation in shell install script to /etc/shellhub-agent
  • Restarts shellhub-agent service at end of installation step instead of ending shellhub-agent at start of installation procedure.
    • Reduces risk of removing device from shellhub tunnel network if agent installation/re-installation/upgrade process was performed over shellhub tunnel

Migration Guide *Optional*

The following guide also applies when migrating from runc-shellhub-agent to the native go-shellhub-agent.

The following steps are only needed if you want to retain the same device unique ID in ShellHub and avoid creating a new pending request.

If the migration steps are not performed, the device will be registered as a new device, and the pending request will need to be accepted.

  1. Manually create /etc/shellhub-agent/ directory
  2. Copy shellhub.key from /opt/shellhub-agent/shelhlub.key or /etc/shellhub.key to /etc/shellhub-agent/shellhub.key
  3. Perform agent installation/upgrade steps as normal
  4. Ensure installation/upgrade was successful and tunnel is active
  5. Able to remove the following /opt/shellhub-agent/ and /etc/shellhub.key and /etc/shellhub-agent.env

@ltan10
ltan10 requested a review from a team as a code owner March 4, 2026 23:59
@ltan10
ltan10 marked this pull request as draft March 5, 2026 00:25
@ltan10
ltan10 force-pushed the chore/group-agent-etc-files branch from fbab19e to a7beeba Compare March 5, 2026 01:01
@ltan10
ltan10 marked this pull request as ready for review March 5, 2026 01:05
@ltan10
ltan10 force-pushed the chore/group-agent-etc-files branch from a7beeba to 1e42d5b Compare March 5, 2026 07:17
@ltan10 ltan10 changed the title chore: organized go based agent config and key into dedicated directory chore(agent): organized go based agent config and key into dedicated directory Mar 5, 2026
@ltan10
ltan10 force-pushed the chore/group-agent-etc-files branch from 1e42d5b to 50f40b7 Compare March 10, 2026 00:43

@otavio otavio left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The biggest problem I foresee here is migrating the field-deployed agents.

So even though I agree with the idea of this change, we need to consider how we will migrate the existing agents to work with this new approach or provide a backward-compatible way for them to keep working.

How do you foresee solving this issue?

@ltan10

ltan10 commented Mar 10, 2026

Copy link
Copy Markdown
Author

With all the testing. I've stumbled upon this myself.
From what I have seen, the migration is fine. as the install script during the migration from roofts to go based binary for the agents already leave the config.json behind and recreate the the new env config file.

The only persistent remaining file is just shellhub.key

Two methods for transition:

  1. Just install/add/upgrade agent as normal using install script as per normal
  • New shellhub.key gets regenerated in the new directory and device appears for pending approval.
  • Requires user to manually remove existing accepted devices due to hostname clashes and accept the new pending devices
    or
  1. Use existing shellhub.key on device by manually transfering it to /etc/shellhub-agent prior to executing install script.
  • install script agent upgrade will just use existing key.

@otavio someone should be able to replicate my findings for confirmation.
I have been generally just been using option 1. And have only tested on the standalone deployment.
If someone can verify that i have changed the correct sections for the other installation methods and test, that would be great.

EDIT:
Should be clear that the above mentioned methods only works with prebuilt agent assets from shellhub releases, so a new release would have to be done for a smooth transition.
Users who pull the repo without the prebuild agent asset release will have to manually build and tweak their install procedure.

@ltan10

ltan10 commented Mar 11, 2026

Copy link
Copy Markdown
Author

@otavio
It later occured to me that my testing was performed using a physical connection and not a shellhub tunnel.

You are correct that users using the tunnel to upgrade their agent will have their tunnel ceremoniously terminated when migrating from the runc binary to go-native binary.

I tried sending the shell install script to background and including using nohup to ignore sighup. However still no luck as the child process executing the agent install did not appear to initiate.

This observation was the same for an standalone agent upgrade made through the tunnel for the following situations

  • go-agent (ungrouped) -> go-agent (ungrouped)
  • go-agent (grouped) -> go-agent (grouped)
  • go-agent (ungrouped) -> go-agent (grouped)

With all tunnels terminated when users used shellhub tunnel to upgrade the agent

@ltan10
ltan10 marked this pull request as draft March 11, 2026 03:43
@ltan10

ltan10 commented Mar 11, 2026

Copy link
Copy Markdown
Author

Found a work around, can someone review please and test the containerised installation/upgrade.

Problem:
Basically found the native binary was affected by sighup and terminates, attempt to execute it using nohup in background did not work and did not provide verbosity to users.

Running the installation under systemd-run worked, but once again was not verbose, and increased complexity in managing the existing service once finished.

Solution:
Instead of stopping shellhub-agent.service at the start of installation, we restart/start the service at the end of installation.
This maintains an active tunnel session till the last moment after binary has been extracted and service is registered.
The session ends however the service is active and tunnel connection is resumed.

Changes:
Agent installation no longer stops the service at the start of installation.
Instead the agent service is restarted at end of install procedure, hence enabling the service does not start the service to prevent duplicate connection request at first installation.

@ltan10
ltan10 marked this pull request as ready for review March 11, 2026 08:44
@ltan10
ltan10 force-pushed the chore/group-agent-etc-files branch from ad7c9d8 to 0d222f4 Compare March 11, 2026 09:06
@ltan10 ltan10 changed the title chore(agent): organized go based agent config and key into dedicated directory Fix(agent): Improve tunnel preservation during agent upgrade when using tunnel. Housekeeping of shellhub-agent files in own directory Mar 31, 2026
@ltan10
ltan10 requested review from a team as code owners March 31, 2026 22:37
@ltan10
ltan10 force-pushed the chore/group-agent-etc-files branch from 88ffb2d to 0d222f4 Compare March 31, 2026 22:37
@ltan10 ltan10 closed this Mar 31, 2026
@ltan10
ltan10 deleted the chore/group-agent-etc-files branch March 31, 2026 22:43
@ltan10
ltan10 restored the chore/group-agent-etc-files branch March 31, 2026 22:44
@ltan10
ltan10 deleted the chore/group-agent-etc-files branch March 31, 2026 22:45
@ltan10
ltan10 restored the chore/group-agent-etc-files branch March 31, 2026 22:47
@ltan10 ltan10 reopened this Mar 31, 2026
@ltan10
ltan10 force-pushed the chore/group-agent-etc-files branch from 0d222f4 to 0d6ec33 Compare March 31, 2026 22:49
@ltan10

ltan10 commented Mar 31, 2026

Copy link
Copy Markdown
Author

The biggest problem I foresee here is migrating the field-deployed agents.

So even though I agree with the idea of this change, we need to consider how we will migrate the existing agents to work with this new approach or provide a backward-compatible way for them to keep working.

How do you foresee solving this issue?

@otavio I noticed that the migration from non native shellhub-agent to the go-native shellhub-agent triggers the same issue of a new pending device join request due to the change in location of shellhub.key from /opt to `/etc/, especially for standalone installations.

I have provided a migration path for users who don't wish to deal with just removing old device and re-accepting the new device above. By moving the shellhub-agent service restart to end of installation, this allows users to upgrade the agent over a shellhub tunnel as the tunnel is not terminated when the service is stopped at the start of the installation process.

@ltan10
ltan10 requested a review from otavio March 31, 2026 23:11
@ltan10
ltan10 force-pushed the chore/group-agent-etc-files branch from 0d6ec33 to 35b4a99 Compare April 1, 2026 22:06
@ltan10
ltan10 force-pushed the chore/group-agent-etc-files branch from 35b4a99 to 5148282 Compare April 15, 2026 00:20
@ltan10
ltan10 force-pushed the chore/group-agent-etc-files branch from 5148282 to ae06a1d Compare April 29, 2026 23:02
@ltan10

ltan10 commented May 15, 2026

Copy link
Copy Markdown
Author

/review

@github-actions

Copy link
Copy Markdown

@ltan10 You are not authorized to request an explicit review. If you believe this PR needs a new automated review round, please tag the @shellhub-io/admin team and a team member can trigger it.

@ltan10

ltan10 commented May 15, 2026

Copy link
Copy Markdown
Author

@shellhub-io/admin

@otavio

otavio commented May 15, 2026

Copy link
Copy Markdown
Member

/review

@github-actions

github-actions Bot commented May 15, 2026

Copy link
Copy Markdown

Claude finished @otavio's task in 23s —— View job


Code Review Complete

Reviewed 3 files across code quality, security, testing, Go patterns, and architecture — no issues found. The code looks good as-is.

Review notes:

  • Path changes (/etc/shellhub.key/etc/shellhub-agent/shellhub.key, /etc/shellhub-agent.env/etc/shellhub-agent/shellhub-agent.env) are consistent across all three files (Go installer, OCI config, shell script).
  • The systemctl enable + systemctl restart split (replacing enable --now) correctly preserves tunnel connections during upgrades. restart on a loaded-but-not-running unit behaves like start, so fresh installs work too.
  • Directory creation with os.MkdirAll before writing the env file is correct.
  • No cross-repo impact found in the cloud repo — no references to the old agent paths exist there.
  • No breaking changes to exported interfaces or pkg/ packages.

If you've addressed feedback and want a new review, tag @shellhub-io/admin and a team member can trigger it.


@ltan10
ltan10 force-pushed the chore/group-agent-etc-files branch from ae06a1d to 6d88f23 Compare June 11, 2026 10:20
@ltan10

ltan10 commented Jun 11, 2026

Copy link
Copy Markdown
Author

Rebased branch. Proposed change completed.

@otavio any chance PR to be merged for next release?

@ltan10
ltan10 force-pushed the chore/group-agent-etc-files branch from 6d88f23 to 772ff28 Compare September 3, 2026 05:29
@otavio

otavio commented Sep 3, 2026

Copy link
Copy Markdown
Member

@ltan10 There is still a conflict that needs to be resolved. Can you take a look?

Besides this, I am wondering what tests of the upgrade you have done?

@ltan10
ltan10 force-pushed the chore/group-agent-etc-files branch from 772ff28 to 6ec1a39 Compare September 3, 2026 22:49
@ltan10

ltan10 commented Sep 3, 2026

Copy link
Copy Markdown
Author

@otavio rebased to the master branch to resolve the conflicts. I've also updated the comments to make more sense.

As for the testing, I have only performed the STANDALONE installation method, in both an upgrade and fresh installation capacity. There is no issues with a fresh agent install.

For an upgrade context, I have tested the upgrade over an active remote ShellHub SSH tunnel multiple times, and was successful each of those times. However like with every remote configuration there is a chance that it may fail.

Every upgrade for runc-agent and go-agent always rewrites it's shellhub-agent.env as is normal behavior.

Observations for the tests are mentioned in previous comments. I'll try to summarize them here.

When upgrading from the runc-agent to "ungrouped" go-agent or "ungrouped" go-agent to this "grouped" version, the existing private key is not migrated. As a result, the new agent will generate a new key, and the device will be treated as a new device registration and await acceptance on the dashboard. Subsequently any existing shellhub.key and shellhub-agent.env from the existing install needs to be manually cleaned.

A migration guide is included in the PR description with steps to preserve the existing key and avoid re-registration.

I have NOT tested the containerized install/upgrade of this new agent. It would be great if someone on the team is able to verify their behavior.

agent installer: avoid service disruption during upgrade over SSH tunnel

- Remove pre-install service disable step
- Enable service without starting it
- Restart service after installation completes

This allows upgrades over an active SSH tunnel while ensuring the
service restarts with the updated binary and re-establishes the tunnel.
@ltan10
ltan10 force-pushed the chore/group-agent-etc-files branch from 6ec1a39 to d5427ac Compare September 6, 2026 23:32

@otavio otavio left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for digging into this, and especially for the systemctl restart-at-the-end finding — the SIGHUP-over-the-tunnel diagnosis in the thread is correct and that part of the change is the right shape.

I've left inline comments on the specific lines. Below are the items that have no line in the diff to hang off.

1. No code-level migration path (blocking)

This is the point I raised earlier in the thread, and it is still open. The PR gives a migration path in prose only: the user must hand-create /etc/shellhub-agent/ and copy the key before upgrading. Every field-deployed agent whose operator doesn't read the release notes re-enrols as a new pending device, leaving the old accepted device behind with a clashing hostname to be deleted by hand.

We can't ship a silent identity change for existing installs. The agent should fall back to the legacy location when the new one is absent — read /etc/shellhub.key (and /opt/shellhub/shellhub.key) if /etc/shellhub-agent/shellhub.key does not exist, and ideally migrate it on first run. Same for the env file. That turns the migration guide into a safety net rather than a prerequisite.

2. agent/pkg/agentd/envfile.go:11 still points at the old path

const defaultEnvFilePath = "/etc/shellhub-agent.env"

This is the only production caller (agent/pkg/agentd/agent.go:171, via LoadConfigFromEnv), so the runtime never reads the new env file except through systemd's EnvironmentFile=. Two consequences:

  • Regression on a fresh install: shellhub-agent login and any manual non-systemd invocation now fail — LoadConfigFromEnv requires SHELLHUB_PRIVATE_KEY/SHELLHUB_SERVER_ADDRESS and the fallback file it was given in 6f57bd0 no longer exists at that path.
  • Split brain on upgrade: systemd reads the new file, while the stale /etc/shellhub-agent.env survives and is what hand-run invocations read — pointing at the old key path.

3. Docs still document the old paths

ui/apps/docs/src/pages/get-started/install/{standalone,docker,podman,raspberry-pi}.mdx still reference /etc/shellhub.key.

4. The new defaults have no test coverage

install.bats passes (82/82), but only because it always exports PRIVATE_KEY (line 20) — it never exercises the defaults, in either direction. That is why the three stale defaults flagged inline slipped through. Worth a case that asserts the default path per install method.

5. Commit messages

Per our code style, the reason behind a workaround lives in the commit message, not in a comment — see the inline notes. Concretely:

  • 0aa4b7b ("Create directory if doesn't exist.") gives no rationale at all for the path move: no migration story, no reason for the directory mode. Its chore(agent) scope also mislabels changes to install.sh and agent/packaging/.
  • d5427ac says "Remove pre-install service disable step" but omits why — the SIGHUP mechanism currently survives only in the banned comment. That explanation is the valuable part; please move it into the commit body in full.

Happy to re-review once the migration fallback and the stale paths are sorted.

Comment thread agent/installer.go

const (
agentEnvFile = "/etc/shellhub-agent.env"
agentEnvFile = "/etc/shellhub-agent/shellhub-agent.env"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agentEnvFile now points at /etc/shellhub-agent/shellhub-agent.env, but agent/pkg/agentd/envfile.go:11 still has:

const defaultEnvFilePath = "/etc/shellhub-agent.env"

That is the only production caller (agent/pkg/agentd/agent.go:171 via LoadConfigFromEnv), so the agent runtime never reads the file this installer writes — only systemd does, through EnvironmentFile=. Result: shellhub-agent login and any manual invocation break on a fresh install, and on an upgrade the stale /etc/shellhub-agent.env keeps pointing at the old key path.

More generally, the layout is now described by two hardcoded strings in two packages. A single seam in agent/pkg/agentd (ConfigDir() / EnvFilePath() / PrivateKeyPath()) consumed by both the installer and the runtime would have made this class of drift impossible — and there are several more instances of it in install.sh below.

Comment thread agent/installer.go
Comment on lines +97 to +101
installCmd.Flags().String(
"private-key",
"/etc/shellhub-agent/shellhub.key",
"Path to the agent private key file",
)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changing the default is the easy half. There is no code that looks at the legacy /etc/shellhub.key (or /opt/shellhub/shellhub.key), so an existing device upgraded without the manual steps from the PR description generates a fresh key here and re-enrols as a new pending device.

Please add a fallback: if the new path does not exist and a legacy key does, use (or migrate) the legacy one. The prose guide should be a convenience, not a prerequisite for keeping a device's identity.

Comment thread agent/installer.go
Comment on lines +136 to +139
// Do not disable/stop the service here. During an upgrade over the SSH tunnel,
// stopping the service disconnects the session and causes the agent to receive
// SIGHUP, which can cause the installation to fail. The service is restarted
// at the end of the install/upgrade procedure.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Our code style allows exactly two kinds of comment: a doc comment on an exported declaration, and a suppression's reason. This is neither — and it's the hardest case of the rule, because it documents code that is no longer here, so nothing in the file is named by it.

The content is genuinely valuable, though: "stopping the service kills the SSH session, the agent takes SIGHUP and the install dies" is exactly the kind of reason that has nowhere in the code to live. Please move it verbatim into the commit body of d5427ac, where git blame reaches it from the line that raised the question. Right now the commit says only "Remove pre-install service disable step", so deleting this comment loses the reason entirely.

Comment thread agent/installer.go
return fmt.Errorf("failed to resolve symlinks: %w", err)
}

if err := os.MkdirAll(filepath.Dir(agentEnvFile), 0755); err != nil {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two things on this line.

Mode. This directory holds the agent private key and a 0600 env file carrying SHELLHUB_INSTALL_KEY and the tenant ID. 0755 leaves it world-traversable and makes the per-file bits the only thing protecting those secrets. 0700 is the right mode for a root-owned secret directory.

Style. The rest of this file uses Go octal literals (0o600 at line 207, 0o644 at line 217); please match.

Suggested change
if err := os.MkdirAll(filepath.Dir(agentEnvFile), 0755); err != nil {
if err := os.MkdirAll(filepath.Dir(agentEnvFile), 0o700); err != nil {

Comment thread agent/installer.go
Comment on lines +167 to +168
// NOTE: Only enable the service here to maintain the SSH tunnel if one is active.
// The service will be restarted after the upgrade.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above — not a doc comment on an exported symbol and not a suppression reason, so it doesn't survive our comment rule. The enable-without---now decision and its pairing with the restart below belongs in the commit message.

Comment thread agent/installer.go
Comment on lines +173 to +176
// Finally, restart the service with the newly installed version.
if err := exec.Command("systemctl", "restart", agentServiceName).Run(); err != nil {
return fmt.Errorf("failed to restart service: %w", err)
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment goes the way of the other two.

On the code: this restart is also a failure seam worth handling. standalone_install() in install.sh reacts to a non-zero exit from install by running $SUDO rm -f "$INSTALL_BIN" (line 428) — but by this point the unit has already been enabled and its service file written. A restart failure therefore leaves an enabled unit whose ExecStart points at a binary that was just deleted, and the next boot fails silently.

Either disable the unit on the error path here, or don't enable until the restart has succeeded.

Also worth capturing stderr from these exec.Command calls (CombinedOutput) — as written, a failure reaches the user as a bare "failed to restart service" with nothing to act on.

Comment thread install.sh

if [ -z "$MODE" ]; then
ARGS="$ARGS -e SHELLHUB_PRIVATE_KEY=${PRIVATE_KEY:-/host/etc/shellhub.key}"
ARGS="$ARGS -e SHELLHUB_PRIVATE_KEY=${PRIVATE_KEY:-/host/etc/shellhub-agent/shellhub.key}"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This default moved, but the matching one 35 lines down in the same function did not — line 244 still reads:

_CKEY="${PRIVATE_KEY:-/host/etc/shellhub.key}"
enroll_agent_interactively "$WRAPPER_PATH" "${_CKEY#/host}"

That is the path enroll_agent_interactively waits on. With no explicit PRIVATE_KEY, the container now writes the key to the new location while the wait loop at line 123 polls the old one, times out after 30s, and reintroduces the login-vs-daemon key race that loop exists to prevent.

(GitHub won't let me comment on line 244 — it isn't in the diff.)

Comment thread install.sh

if [ -z "$MODE" ]; then
ARGS="$ARGS -e SHELLHUB_PRIVATE_KEY=${PRIVATE_KEY:-/host/etc/shellhub.key}"
ARGS="$ARGS -e SHELLHUB_PRIVATE_KEY=${PRIVATE_KEY:-/host/etc/shellhub-agent/shellhub.key}"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same problem as the podman path: line 333 still has _CKEY="${PRIVATE_KEY:-/host/etc/shellhub.key}", so the enrollment wait polls the old location while the container writes the new one.

(Line 333 isn't in the diff, so I can't comment on it directly.)

Comment thread install.sh
sudo snap set shellhub server-address="$SERVER_ADDRESS"
sudo snap set shellhub tenant-id="$TENANT_ID"
sudo snap set shellhub private-key="${PRIVATE_KEY:-/etc/shellhub.key}"
sudo snap set shellhub private-key="${PRIVATE_KEY:-/etc/shellhub-agent/shellhub.key}"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This one is correct, but its sibling in standalone_install() was missed — line 437 still reads:

enroll_agent_interactively "$SUDO $INSTALL_BIN" "${PRIVATE_KEY:-/etc/shellhub.key}"

So the native installer writes the key to /etc/shellhub-agent/shellhub.key (per the new flag default) while enrollment waits on /etc/shellhub.key.

That is 3 of 6 defaults left stale across this file. Rather than chasing each literal, consider a single variable near the top — DEFAULT_PRIVATE_KEY=/etc/shellhub-agent/shellhub.key — that every method derives from.

(Line 437 isn't in the diff, hence the comment here.)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants