An application that reads passwords from a configuration file and cannot call the Stronghold API can be connected without code changes. Stronghold Agent authenticates to Stronghold, renders the configuration file from a template, and reloads the application when secrets change.

Goal

Set up Stronghold Agent under systemd on a virtual machine. Agent logs in with AppRole using a one-time wrapped secret_id, builds the application.properties file, and runs systemctl reload myapp when secrets change.

Prerequisites

Before you start, prepare the following:

  • A Linux virtual machine with systemd and the myapp application running as the myapp.service service.
  • The Stronghold binary on the VM (/usr/local/bin/stronghold in the examples) and network access to Stronghold.
  • A Stronghold token with permissions to configure AppRole, policies, and KV.
  • KV version 2 enabled at the secret path.

Step 1. Store the secret and create a policy

Write the database credentials to KV and create a policy that allows reading them:

d8 stronghold kv put -mount=secret myapp/config db_user=app_user db_password='S3cure-P@ss'

d8 stronghold policy write myapp-vm - <<'POLICY'
path "secret/data/myapp/config" {
  capabilities = ["read"]
}
POLICY

Step 2. Create an AppRole role

Enable the AppRole auth method and create a role for the VM:

d8 stronghold auth enable approle

d8 stronghold write auth/approle/role/myapp-vm token_policies=myapp-vm token_ttl=1h token_max_ttl=24h secret_id_ttl=720h secret_id_bound_cidrs="10.0.10.15/32"

The secret_id_bound_cidrs parameter allows login only from the VM address. secret_id_ttl limits how long Agent can log in again with one secret_id.

Step 3. Deliver role_id and a wrapped secret_id to the VM

Agent logs in with role_id and a one-time wrapped secret_id. Prepare both files and copy them to the VM.

  1. Get the role_id. It is not a secret and can be distributed by a configuration management system:

    d8 stronghold read -field=role_id auth/approle/role/myapp-vm/role-id > role-id
  2. Issue a wrapped secret_id. Instead of the secret_id itself, a one-time token with a 10-minute TTL is returned:

    d8 stronghold write -wrap-ttl=10m -field=wrapping_token -f auth/approle/role/myapp-vm/secret-id > secret-id
  3. Copy both files to the VM into /etc/stronghold-agent/ and restrict access. The directory must belong to the stronghold-agent user: Agent deletes the secret_id file after reading it.

    sudo install -d -o stronghold-agent -g stronghold-agent -m 0750 /etc/stronghold-agent
    sudo install -o stronghold-agent -g stronghold-agent -m 0640 role-id /etc/stronghold-agent/role-id
    sudo install -o stronghold-agent -g stronghold-agent -m 0600 secret-id /etc/stronghold-agent/secret-id

If the wrapping token is intercepted and unwrapped by someone else, Agent cannot unwrap it, which indicates an incident. The verification procedure is described in Response wrapping.

Step 4. Create a template

Create the /etc/myapp/templates/application.properties.ctmpl file:

{{ with secret "secret/data/myapp/config" }}
spring.datasource.username={{ .Data.data.db_user }}
spring.datasource.password={{ .Data.data.db_password }}
{{ end }}

Step 5. Configure Agent

Create the /etc/stronghold-agent/agent.hcl file:

stronghold {
  address = "https://stronghold.example.com"
}

auto_auth {
  method "approle" {
    mount_path = "auth/approle"
    config = {
      role_id_file_path                   = "/etc/stronghold-agent/role-id"
      secret_id_file_path                 = "/etc/stronghold-agent/secret-id"
      secret_id_response_wrapping_path    = "auth/approle/role/myapp-vm/secret-id"
      remove_secret_id_file_after_reading = true
    }
  }

  sink "file" {
    config = {
      path = "/var/run/stronghold-agent/token"
      mode = 0600
    }
  }
}

template_config {
  static_secret_render_interval = "5m"
}

template {
  source               = "/etc/myapp/templates/application.properties.ctmpl"
  destination          = "/etc/myapp/application.properties"
  perms                = "0640"
  group                = "myapp"
  command              = "/usr/bin/systemctl reload myapp"
  command_timeout      = "30s"
  error_on_missing_key = true
}
  • secret_id_response_wrapping_path: Agent recognizes the wrapped secret_id, checks its creation path, and unwraps it.
  • remove_secret_id_file_after_reading = true: the secret_id file is deleted after reading.
  • static_secret_render_interval: how often Agent re-reads static KV secrets.
  • perms and group: the file belongs to the stronghold-agent user and the myapp group, so the application can read it. Agent can assign only a group that the stronghold-agent user is a member of. Add the user to the group with sudo usermod -aG myapp stronghold-agent.
  • command: runs after each write of the file. The command is run without sudo: the Agent unit runs with NoNewPrivileges=true, so sudo does not work. The permission to reload the application is granted by a polkit rule in step 6.

The unwrapped secret_id is kept only in Agent memory. After Agent or the VM restarts, or when secret_id_ttl expires, Agent needs a new wrapped secret_id. Build issuing and delivering secret_id into your deployment process, for example in CI/CD.

Step 6. Run Agent under systemd

Configure the Agent unit, grant Agent the permission to reload the application, and start the service.

  1. Create the /etc/systemd/system/stronghold-agent.service unit file following the example in Running and managing Agent. In ReadWritePaths, list the /var/run/stronghold-agent and /etc/myapp directories.

  2. Allow Agent to delete the secret_id file. The unit uses ProtectSystem=strict, so add the /etc/stronghold-agent directory to ReadWritePaths in a drop-in file. Run sudo systemctl edit stronghold-agent and add the following lines:

    [Service]
    ReadWritePaths=/etc/stronghold-agent
  3. Allow the stronghold-agent user to reload only myapp.service. Create the /etc/polkit-1/rules.d/50-stronghold-agent-myapp.rules file:

    polkit.addRule(function(action, subject) {
        if (action.id == "org.freedesktop.systemd1.manage-units" &&
            subject.user == "stronghold-agent" &&
            action.lookup("unit") == "myapp.service" &&
            action.lookup("verb") == "reload") {
            return polkit.Result.YES;
        }
    });

    polkit reads the rules files automatically. The unit and verb details are passed by systemd 226 and later. If the application does not support reload, replace "reload" with "restart" in the rule and reload with restart in command.

  4. Start the service:

    sudo systemctl daemon-reload
    sudo systemctl enable --now stronghold-agent

Verification

Check that Agent has logged in, rendered the file, and reacts to secret changes.

  1. Check the Agent log: it must contain messages about successful authentication and rendering:

    sudo journalctl -u stronghold-agent -n 50
  2. Make sure the secret file is deleted and the configuration is built:

    ls -la /etc/stronghold-agent/secret-id /etc/myapp/application.properties
  3. Change the secret and wait for the file to be re-rendered and the application to reload (no longer than static_secret_render_interval):

    d8 stronghold kv patch -mount=secret myapp/config db_password='N3w-P@ss'
    sudo journalctl -u myapp -f

Cleanup

To remove the example configuration, follow these steps:

  1. Stop and disable Agent: sudo systemctl disable --now stronghold-agent.

  2. Delete the /etc/stronghold-agent/role-id and /etc/myapp/application.properties files and the token file in /var/run/stronghold-agent/.

  3. Delete the /etc/polkit-1/rules.d/50-stronghold-agent-myapp.rules file and the Agent drop-in file: sudo systemctl revert stronghold-agent.

  4. Delete the role and the policy:

    d8 stronghold delete auth/approle/role/myapp-vm
    d8 stronghold policy delete myapp-vm

Additional resources