> ## Documentation Index
> Fetch the complete documentation index at: https://docs.gcore.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Secret rotation with slots

export const MethodSection = ({children}) => children ?? null;

export const MethodSwitch = ({children}) => {
  const tabs = React.Children.toArray(children).map(c => {
    if (!c || !c.props) return null;
    if (c.props.id) return c;
    const inner = c.props.children;
    if (inner && inner.props && inner.props.id) return inner;
    return null;
  }).filter(Boolean);
  const firstId = tabs.length > 0 ? tabs[0].props.id : "";
  const [active, setActive] = React.useState(firstId);
  React.useEffect(() => {
    try {
      const saved = localStorage.getItem("gcore_docs_method");
      if (saved && tabs.find(t => t.props.id === saved)) {
        setActive(saved);
      }
    } catch (_) {}
  }, []);
  React.useEffect(() => {
    try {
      document.querySelectorAll("h2[id], h3[id]").forEach(heading => {
        const visible = heading.offsetParent !== null;
        document.querySelectorAll(`a[href="#${heading.id}"]`).forEach(link => {
          if (link.closest("h1,h2,h3,h4,h5,h6")) return;
          const li = link.closest("li");
          if (li) li.style.display = visible ? "" : "none";
        });
      });
    } catch (_) {}
    window.dispatchEvent(new Event("scroll"));
  }, [active]);
  const handleClick = id => {
    setActive(id);
    try {
      localStorage.setItem("gcore_docs_method", id);
    } catch (_) {}
  };
  return <div>
      <div className="not-prose flex gap-0 border-b border-zinc-200 dark:border-zinc-800 mb-8 mt-2" role="tablist">
        {tabs.map(tab => {
    const isActive = active === tab.props.id;
    return <button key={tab.props.id} role="tab" aria-selected={isActive} onClick={() => handleClick(tab.props.id)} className={["px-4 py-2 text-sm font-medium border-b-2 -mb-px transition-colors cursor-pointer", isActive ? "border-primary text-primary" : "border-transparent text-zinc-500 hover:text-zinc-800 dark:hover:text-zinc-200"].join(" ")}>
              {tab.props.label}
            </button>;
  })}
      </div>

      {tabs.map(tab => <div key={tab.props.id} style={{
    display: active === tab.props.id ? "" : "none"
  }}>
          {tab.props.children}
        </div>)}
    </div>;
};

<MethodSwitch>
  <MethodSection id="portal" label="Customer Portal">
    A secret can hold multiple encrypted values in separate slots. Each slot has a numeric **Slot index** and an independently encrypted value.

    ## Slot selection logic

    The SDK selects a slot based on its index. Which slot is returned depends on the function used:

    * **`get_secret(name)`** — always returns the value from the slot with the **highest** index
    * **`get_effective_at(name, n)`** — returns the value from the slot with the **highest index that is ≤ n**

    ## Slot indices for password rotation

    In this pattern, slot indices are arbitrary integers that represent password versions. Applications store the slot index used to sign a token in the token payload, then pass that index to `get_effective_at` when validating.

    The secret below has two slots:

    <Frame>
      <img src="https://mintcdn.com/gcore/MFAXqa6tFHU8UZIW/images/docs/fastedge/secrets-manager/slots/slots-example-indices.png?fit=max&auto=format&n=MFAXqa6tFHU8UZIW&q=85&s=1878a8de9497162c07b41eb3c1c08e9c" alt="Create secret form with two slots — index 0 for original password and index 5 for updated password" width="1536" height="674" data-path="images/docs/fastedge/secrets-manager/slots/slots-example-indices.png" />
    </Frame>

    With this configuration:

    * `get_effective_at("token-secret", 0)` returns `original_password`
    * `get_effective_at("token-secret", 3)` returns `original_password` (highest slot ≤ 3 is slot 0)
    * `get_effective_at("token-secret", 5)` returns `updated_password`
    * `get_effective_at("token-secret", 7)` returns `updated_password` (highest slot ≤ 7 is slot 5)

    Tokens signed with the old password carry index 0 and continue to validate correctly. New tokens carry index 5 and use the updated password. Both coexist without requiring any immediate migration.

    ## Unix timestamps for time-based rotation

    In this pattern, slot indices are Unix timestamps indicating when each password version becomes active. Applications pass the token issue time (`iat` claim) to `get_effective_at`.

    <Frame>
      <img src="https://mintcdn.com/gcore/MFAXqa6tFHU8UZIW/images/docs/fastedge/secrets-manager/slots/slots-example-timestamps.png?fit=max&auto=format&n=MFAXqa6tFHU8UZIW&q=85&s=9828d0e63927c3c0d5d4c68bc6b6d2ef" alt="Create secret form with two slots — index 0 for original password and index 1741790697 for new password" width="1536" height="674" data-path="images/docs/fastedge/secrets-manager/slots/slots-example-timestamps.png" />
    </Frame>

    With slot 0 holding `original_password` and slot `1741790697` (Wed Mar 12 2025 14:44:57 UTC) holding `new_password`:

    * Any token with `iat` before `1741790697` validates against `original_password`
    * Any token issued after that timestamp validates against `new_password`

    Rotation happens automatically at the specified time — no redeployment required.
  </MethodSection>

  <MethodSection id="api" label="REST API">
    Slots are stored as part of the secret object. Use `PATCH` to add rotation slots without removing existing ones. Use `PUT` only when replacing the entire secret configuration.

    <p>All requests authenticate with an [API token](/account-settings/api-tokens). Set it as an environment variable before running the examples:</p>

    ```bash theme={null}
    export GCORE_API_KEY="{YOUR_API_KEY}"
    ```

    ## Create a secret with multiple slots

    Pass all slots in a single request to create a pre-configured rotation setup:

    ```bash theme={null}
    curl -X POST https://api.gcore.com/fastedge/v1/secrets \
      -H "Authorization: APIKey $GCORE_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{
        "name": "token-secret",
        "comment": "Password for token validation",
        "secret_slots": [
          {"slot": 0,          "value": "original_password"},
          {"slot": 1741790697, "value": "new_password"}
        ]
      }'
    ```

    ## Add a rotation slot

    To add a new slot to an existing secret without removing the current ones, use `PATCH` with only the new slot:

    ```bash theme={null}
    curl -X PATCH https://api.gcore.com/fastedge/v1/secrets/SECRET_ID \
      -H "Authorization: APIKey $GCORE_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{
        "secret_slots": [
          {"slot": 1741790697, "value": "new_password"}
        ]
      }'
    ```

    `PATCH` adds or updates only the slots listed in the request — existing slots at other indices are preserved.

    <Warning>
      `PUT` replaces the entire secret including all slots. Any slot not included in a `PUT` request is permanently removed. Use `PATCH` for rotation to preserve existing slots.
    </Warning>

    ## Remove an old slot after rotation is complete

    Once all clients have migrated to the new password, remove the old slot by sending a `PUT` with only the current slot:

    ```bash theme={null}
    curl -X PUT https://api.gcore.com/fastedge/v1/secrets/SECRET_ID \
      -H "Authorization: APIKey $GCORE_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{
        "name": "token-secret",
        "secret_slots": [
          {"slot": 1741790697, "value": "new_password"}
        ]
      }'
    ```

    This removes slot 0 and leaves only the new slot. After this, `get_secret` and `get_effective_at` with any index ≥ 0 both return the new password.
  </MethodSection>
</MethodSwitch>
