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

# Rotate Git SSH deploy keys with the API

> Use the API to rotate SSH deploy keys without interrupting Git operations.

export const keyInstructions_4 = "Paste the contents of omni_deploy_key.pub."

export const keyInstructions_3 = "Paste the contents of omni_deploy_key.pub."

export const keyInstructions_2 = "Paste the contents of omni_deploy_key.pub."

export const keyInstructions_1 = "Paste the contents of omni_deploy_key.pub."

export const keyInstructions_0 = "Paste the contents of omni_deploy_key.pub."

export const categoryIcons = {
  'administration': 'lock',
  'api': 'terminal',
  'connections': 'database',
  'dashboards': 'table-columns',
  'embed': 'code',
  'errors': 'exclamation',
  'migration': 'angles-right',
  'modeling': 'wrench',
  'patterns': 'plus',
  'schedules & alerts': 'envelope',
  'visualizations': 'chart-column',
  'workbooks': 'book'
};

export const GuideSidebar = ({category, relatedLinks, updatedDate}) => {
  const [progress, setProgress] = React.useState(0);
  React.useEffect(() => {
    const sidebar = document.querySelector('.guide-sidebar');
    if (!sidebar) return;
    let container = sidebar.parentElement;
    while (container && !container.querySelector('.guide-header')) {
      container = container.parentElement;
    }
    if (container && !container.classList.contains('guide-page-layout')) {
      container.classList.add('guide-page-layout');
    }
  }, []);
  React.useEffect(() => {
    const handleScroll = () => {
      const scrollTop = window.scrollY;
      const docHeight = document.documentElement.scrollHeight - window.innerHeight;
      const scrollPercent = docHeight > 0 ? scrollTop / docHeight * 100 : 0;
      setProgress(Math.min(100, Math.max(0, scrollPercent)));
    };
    window.addEventListener('scroll', handleScroll, {
      passive: true
    });
    handleScroll();
    return () => window.removeEventListener('scroll', handleScroll);
  }, []);
  const icon = category ? categoryIcons[category.toLowerCase()] || 'book' : 'book';
  return <aside className="guide-sidebar">
      <div className="guide-sidebar-content">
        <a href="/guides" className="guide-sidebar-back">
          <Icon icon="arrow-left" iconType="solid" size={14} />
          <span>All guides</span>
        </a>

        <div className="guide-sidebar-section">
          <div className="guide-sidebar-label">Progress</div>
          <div className="guide-sidebar-progress">
            <div className="guide-mascot">
              <svg viewBox="0 0 688 690" width="48" height="48">
                <defs>
                  <clipPath id="progressClip">
                    <rect x="0" y={0} width="688" height={progress * 6.9} />
                  </clipPath>
                </defs>

                {}
                <path d="M343.67 1.5C542.684 1.5 685.84 149.351 685.84 344.84C685.84 540.328 542.685 688.18 343.67 688.18C144.655 688.18 1.5 540.318 1.5 344.84C1.50007 149.361 144.655 1.50005 343.67 1.5Z" fill="#FCFCF7" stroke="#FF5FA2" strokeWidth="3" />

                {}
                <path d="M343.67 0C143.81 0 0 148.55 0 344.84C0 541.13 143.81 689.68 343.67 689.68C543.53 689.68 687.34 541.14 687.34 344.84C687.34 148.54 543.53 0 343.67 0Z" fill="#FF5FA2" clipPath="url(#progressClip)" />

                {}
                <path d="M337.89 319.29C337.89 336.75 322.49 350.14 302.81 349.83C286.18 349.57 273.89 337.29 274.37 321.45C274.88 304.82 290.91 290.88 309.98 290.44C325.69 290.09 337.88 302.69 337.88 319.29H337.89Z" fill="#4D122C" />
                <path d="M566.17 319.29C566.17 336.75 550.77 350.14 531.09 349.83C514.46 349.57 502.17 337.29 502.65 321.45C503.16 304.82 519.19 290.88 538.26 290.44C553.97 290.09 566.16 302.69 566.16 319.29H566.17Z" fill="#4D122C" />
                <path d="M367.74 342.07C360.22 346.32 359.4 354.9 370.62 366.4C381.85 377.9 399.76 389.56 420.81 389.18C441.88 389.1 460.67 377.72 472.47 363.53C473.83 361.93 478.84 356.88 478.51 351.07C478.32 348.35 476.17 341.19 467.83 341.38C463.46 341.44 461.21 343.68 456.69 347.36C445.2 356.14 432.7 361.21 420.56 361.27C408.43 361.43 395.68 356.17 385.39 347.22C380.32 342.81 375.25 337.82 367.74 342.07Z" fill="#4D122C" />
              </svg>
            </div>
            <span className="guide-sidebar-progress-text">{Math.round(progress)}%</span>
          </div>
        </div>

        {category && <div className="guide-sidebar-section">
            <div className="guide-sidebar-label">Category</div>
            <div className="guide-sidebar-category">
              <Icon icon={icon} iconType="solid" size={14} />
              <span>{category}</span>
            </div>
          </div>}

        {updatedDate && <div className="guide-sidebar-section">
            <div className="guide-sidebar-label">Last updated</div>
            <div className="guide-sidebar-date">{updatedDate}</div>
          </div>}

        {relatedLinks && relatedLinks.length > 0 && <div className="guide-sidebar-section">
            <div className="guide-sidebar-label">Related</div>
            <ul className="guide-sidebar-links">
              {relatedLinks.map((link, index) => <li key={index}>
                  <a href={link.href}>{link.title}</a>
                </li>)}
            </ul>
          </div>}
      </div>
    </aside>;
};

export const GuideTitle = ({title}) => {
  return <div className="guide-header">
      <h1 className="guide-title">{title}</h1>
    </div>;
};

<GuideSidebar
  categoryIcons={categoryIcons}
  category="api"
  updatedDate="July 2026"
  relatedLinks={[
{ title: "API authentication", href: "/api/authentication" },
{ title: "Git integration settings", href: "/integrations/git/settings" },
{ title: "Update git configuration", href: "/api/model-git-configuration/update-git-configuration" },
{ title: "Git integration overview", href: "/integrations/git" }
]}
/>

<GuideTitle title="Rotate Git SSH deploy keys with the API" />

While Omni generates SSH deploy keys automatically when you connect a Git repository, you can also supply your own RSA or ED25519 private key via the API.

If you need to rotate keys—for example, to meet a compliance schedule—the API approach allows you to avoid downtime during the rotation. You first authorize the new public key with your Git provider, supply the private key to Omni, and then remove the unneeded public key from Git. This ensures that both keys work during the transition. The new key will take effect on the next Git operation.

## Requirements

To follow the steps in this guide, you'll need:

* An [Omni API key](/api/authentication)
* **Connection Admin** permissions on the model
* A Git-connected model using **SSH authentication**
* The shared model's `modelId`, which you can retrieve using the [List models](/api/models/list-models) endpoint
* `ssh-keygen` installed locally to generate keypairs

## Supported key types

Omni accepts RSA and ED25519 private keys in PEM format:

* **OpenSSH format** (default from `ssh-keygen`)
* **PKCS#1 format**
* **PKCS#8 format**

Keys can be passphrase-protected or unencrypted. When you supply an encrypted key, Omni uses the passphrase once to decrypt it, then stores the key under its own encryption at rest. The passphrase is not retained.

<Steps titleSize="h2">
  <Step title="Generate a new keypair">
    Use `ssh-keygen` to generate a new RSA or ED25519 keypair.

    <Tabs>
      <Tab title="ED25519">
        ```bash wrap theme={null}
        ssh-keygen -t ed25519 -C "omni-deploy-key-rotation-$(date +%Y%m%d)" -f omni_deploy_key
        ```
      </Tab>

      <Tab title="RSA">
        ```bash wrap theme={null}
        ssh-keygen -t rsa -b 4096 -C "omni-deploy-key-rotation-$(date +%Y%m%d)" -f omni_deploy_key
        ```
      </Tab>
    </Tabs>

    When prompted for a passphrase:

    * **To leave the key unencrypted**, press `Enter`.
    * **To encrypt the key**, provide a passphrase. This is more secure at rest, but you'll need to supply it to Omni.

    This generates two files:

    * `omni_deploy_key` — The private key, which you'll supply to Omni
    * `omni_deploy_key.pub` — The public key, which you'll authorize with your Git provider
  </Step>

  <Step title="Authorize the public key with your Git provider">
    Add the public key (`omni_deploy_key.pub`) to your Git repository as a deploy key with **write access**. This step ensures the new key is authorized before you supply the private key to Omni.

    <Tabs>
      <Tab title="GitHub" icon="github">
        1. In the GitHub repository, click the **Settings** tab.
        2. Click **Deploy keys**, located in the **Security** section of the left navigation.
        3. Click **Add deploy key**.
        4. Fill in the fields as follows:
           * **Title** - Enter a descriptive title to help you identify what the key is used for. For example, *Omni Snowflake Model*
           * **Key** - {keyInstructions_0}
           * **Allow write access** - Check this box, which will allow Omni to push changes made in Omni to the repository
        5. Click **Add key**.
      </Tab>

      <Tab title="GitLab" icon="gitlab">
        1. Create a **project deploy key** for the GitLab repository by following [GitLab's documentation](https://docs.gitlab.com/ee/user/project/deploy_keys/#create-a-project-deploy-key).
                   <Note>
                     GitLab recommends using a [service account](https://docs.gitlab.com/user/profile/service_accounts/) when creating a deploy key so that the key will be unaffected if the [user leaves the organization](https://docs.gitlab.com/user/profile/service_accounts/).
                   </Note>
        2. Fill in the deploy key fields as follows:
           * **Title** - Enter a descriptive title to help you identify what the key is used for. For example, *Omni Snowflake Model*
           * **Key** - {keyInstructions_1}
           * **Grant write permissions to this key** - Check this box, which will allow Omni to push changes made in Omni to the repository
        3. Click **Add key**.
      </Tab>

      <Tab title="Azure DevOps" icon="microsoft">
        1. Browse to the Azure DevOps web portal (ex: `https://dev.azure.com/<your-org-name>/`).
        2. Click the **User settings** icon next to your avatar in the top right corner of the page.
        3. Click **SSH public keys**.
        4. On the page that displays, click **+ New Key**.
        5. In the **Add New SSH Key** panel that displays, fill in the following:
           * **Name** - Enter a descriptive name, such as *Omni Snowflake Model*
           * **Public Key Data** - {keyInstructions_2}
        6. Click **Add**.
      </Tab>

      <Tab title="Bitbucket Cloud" icon="cloud">
        Bitbucket Cloud access keys are read-only. To allow Omni to push changes, you'll need to add the SSH key to a user account with write access to the repository. We recommend using a service account for this purpose.

        1. Create a service account with write access to the repository.
        2. In the service account, navigate to **Personal Bitbucket settings > SSH keys**.
        3. Click **Add key**.
        4. Fill in the fields as follows:
           * **Label** - Give your new SSH key a descriptive label
           * **Key** - {keyInstructions_3}
        5. Click **Add key** to save the key.
      </Tab>

      <Tab title="Bitbucket Data Center" icon="database">
        1. In the Bitbucket repository, click **Repository settings**.
        2. Click **Access keys**, located in the left navigation.
        3. Click **Add key**.
        4. Fill in the fields as follows:
           * **Key** - {keyInstructions_4}
           * **Permission** - Select **Write** to allow Omni to push changes made in Omni to the repository
        5. Click **Add key**.
      </Tab>
    </Tabs>
  </Step>

  <Step title="Supply the private key to Omni with the API">
    Next, call the [Update Git configuration](/api/model-git-configuration/update-git-configuration) API and provide the private key.

    <Tabs>
      <Tab title="Unencrypted keys">
        In the terminal, navigate to the same location where the `omni_deploy_key` file was saved. Then, run the following:

        ```bash wrap theme={null}
        curl -X PATCH https://<your-subdomain>.omniapp.co/api/v1/models/<MODEL_ID>/git \
          -H "Authorization: Bearer <YOUR_API_TOKEN>" \
          -H "Content-Type: application/json" \
          -d "$(jq -n --arg key "$(cat omni_deploy_key)" '{deployPrivateKey: $key}')"
        ```

        Replace the following variables:

        * `<your-subdomain>` - Your Omni subdomain
        * `<MODEL_ID>` - The shared model's UUID
        * `<YOUR_API_TOKEN>` - Your Omni API key

        A successful response will include the derived public key in the `publicKey` field.
      </Tab>

      <Tab title="Passphrase-protected keys">
        1. In the terminal, navigate to the same location where the `omni_deploy_key` file was saved.
        2. Run the following to create a plain shell variable for the passphrase:

           ```bash wrap theme={null}
           printf "Passphrase: " && read -rs DEPLOY_KEY_PASSPHRASE && echo
           ```
        3. When prompted, enter the passphrase.
        4. Then, run the following to supply Omni with the private key and passphrase:

           ```bash wrap theme={null}
           curl -X PATCH https://<your-subdomain>.omniapp.co/api/v1/models/<MODEL_ID>/git \
             -H "Authorization: Bearer <YOUR_API_TOKEN>" \
             -H "Content-Type: application/json" \
             -d "$(jq -n \
                   --arg key "$(cat omni_deploy_key)" \
                   --arg passphrase "$DEPLOY_KEY_PASSPHRASE" \
                   '{deployPrivateKey: $key, deployKeyPassphrase: $passphrase}')"
           ```

           Replace the following:

           * `<your-subdomain>` - Your Omni subdomain
           * `<MODEL_ID>` - The shared model's UUID
           * `<YOUR_API_TOKEN>` - Your Omni API key

           A successful response will include the derived public key in the `publicKey` field.
        5. After you receive a successful response, run the following to unset the passphrase variable:

           ```bash theme={null}
           unset DEPLOY_KEY_PASSPHRASE
           ```
      </Tab>
    </Tabs>

    <Tip>
      If you run into issues, see the [Troubleshooting section](#troubleshooting).
    </Tip>
  </Step>

  <Step title="Verify the new key works">
    To verify the setup, navigate back to the **Git settings** page in Omni. Click the **Test git connection** button near the top of the page to test the connection.

    Alternatively, call the [Sync model with Git repository](/api/model-git-configuration/sync-model-with-git-repository) endpoint:

    ```bash theme={null}
    curl -X POST https://<your-subdomain>.omniapp.co/api/v1/models/<MODEL_ID>/git/sync \
      -H "Authorization: Bearer <YOUR_API_TOKEN>"
    ```

    If the connection test or sync succeeds, the new key is working correctly.
  </Step>

  <Step title="Remove the old key from your Git provider">
    Once you've confirmed the new key works, remove the old deploy key from your Git provider's settings.
  </Step>
</Steps>

<h2 id="troubleshooting">
  Troubleshooting
</h2>

<AccordionGroup>
  <Accordion title="400 error: Invalid key format">
    The private key must be in PEM format (OpenSSH, PKCS#1, or PKCS#8) and be a valid RSA or ED25519 key. Common causes:

    * The key file is corrupted or incomplete
    * The key is in an unsupported format (for example, PuTTY `.ppk` format)
    * The key file contains extra whitespace or characters

    Regenerate the key with `ssh-keygen` and try again.
  </Accordion>

  <Accordion title="400 error: Wrong passphrase">
    The passphrase you provided does not match the passphrase used to encrypt the private key. Verify the passphrase and retry.

    If you've forgotten the passphrase, you'll need to generate a new keypair and start the rotation process again.
  </Accordion>

  <Accordion title="400 error: Encrypted key requires deployKeyPassphrase">
    The private key is passphrase-protected, but you didn't supply `deployKeyPassphrase` in the request. Either:

    * Provide the passphrase via the `deployKeyPassphrase` field
    * Or generate a new unencrypted key (press Enter when `ssh-keygen` prompts for a passphrase)
  </Accordion>

  <Accordion title="400 error: deployKeyPassphrase cannot be provided without deployPrivateKey">
    You included `deployKeyPassphrase` in the request body without `deployPrivateKey`. Remove `deployKeyPassphrase` or include `deployPrivateKey`.
  </Accordion>

  <Accordion title="400 error: deployPrivateKey not supported with authMethod: https_token">
    Custom deploy keys are only supported for SSH authentication. If your model uses HTTPS token authentication (`authMethod: https_token`), you cannot supply a custom deploy key. HTTPS token connections rotate via the `token` field instead.

    To use custom deploy keys, switch your model to SSH authentication.
  </Accordion>

  <Accordion title="Sync fails after rotation">
    If Git operations fail after rotating the key:

    1. Verify the public key was added to your Git provider with **write access**.
    2. Verify the public key in the API response matches the public key you added to your Git provider (compare the `publicKey` field in the response to the contents of `omni_deploy_key.pub`).
    3. Check your Git provider's webhook logs for authentication errors.

    If the issue persists, re-add the public key to your Git provider and retry the rotation.
  </Accordion>
</AccordionGroup>

## Next steps

* [Git integration settings](/integrations/git/settings) to configure pull request behavior, base branches, and more
* [Git integration overview](/integrations/git) for the full feature set
* [Update Git configuration API reference](/api/model-git-configuration/update-git-configuration) for all available fields
