{"id":46033,"date":"2026-11-03T12:11:54","date_gmt":"2026-11-03T11:11:54","guid":{"rendered":"https:\/\/www.credativ.de\/?p=46033"},"modified":"2026-11-03T12:16:05","modified_gmt":"2026-11-03T11:16:05","slug":"temporary-root-ansible-in-gitlab-ci-without-static-ssh-keys","status":"publish","type":"post","link":"https:\/\/www.credativ.de\/en\/blog\/automation\/temporary-root-ansible-in-gitlab-ci-without-static-ssh-keys\/","title":{"rendered":"Temporary Root: Ansible in GitLab CI without Static SSH Keys"},"content":{"rendered":"&#13;\n<p class=\"wp-block-paragraph\">Anyone who manages their servers with Ansible will eventually face the question of whether the playbooks can also run automatically from a CI\/CD pipeline. If the Ansible repository is already in GitLab, GitLab CI is ideal for this: repository, merge requests, and pipeline are all in one place. This applies particularly to a GitOps workflow: the Git repository describes the desired state of the infrastructure, changes are reviewed via merge request, the pipeline shows <code>ansible-playbook --check --diff<\/code> in advance what would change, and deploys the change after the merge. A regular check run also detects discrepancies between the repository and the servers.<\/p>&#13;\n&#13;\n<p class=\"wp-block-paragraph\">For this to work, Ansible must be able to log in to the servers via SSH, usually with a user who obtains <code>sudo<\/code> root privileges. In this blog post, we want to look at how this access can be achieved without a static SSH key.<\/p>&#13;\n&#13;\n<p class=\"wp-block-paragraph\">This is made possible by a GitLab CI feature: every job can request a signed ID token from GitLab, which contains information such as project and branch. We use this token to obtain an SSH certificate from HashiCorp Vault that is valid for 30 minutes. To do this, each job generates its own key pair and has it signed by an SSH CA in Vault. Vault can be configured to issue these certificates only for approved projects and protected branches.<\/p>&#13;\n&#13;\n<h2 class=\"wp-block-heading\">Problem Statement<\/h2>&#13;\n&#13;\n<p class=\"wp-block-paragraph\">The common approach is as follows: a private SSH key is stored as a protected CI\/CD variable in GitLab, and the corresponding public key is placed on all servers in the <code>authorized_keys<\/code>. This is quickly set up but raises a number of questions during operation.<\/p>&#13;\n&#13;\n<p class=\"wp-block-paragraph\">Firstly, a key in the <code>authorized_keys<\/code> is valid indefinitely. If access needs to be revoked, the key must be removed from all servers. Which jobs are allowed to use the key is solely governed by the settings of the CI\/CD variable in GitLab. Anyone who can read the variable has access to all servers, regardless of which branch or in what context a job is running.<\/p>&#13;\n&#13;\n<p class=\"wp-block-paragraph\">Closely related to this is the question of who else is actually using the key. A private key can be copied at will and used from any machine, and this is exactly what happens in everyday life: it is copied to a laptop to reproduce a failed run, reused in another project or script, or ends up in the password manager of a colleague who has since left the team. After some time, no one knows for sure where copies still exist. This also makes it unclear who can log in to the servers as root with this key.<\/p>&#13;\n&#13;\n<p class=\"wp-block-paragraph\">In addition, there is traceability. All jobs log in with the same key. In the server&#8217;s log, a login from the pipeline cannot be assigned to a specific job, nor can it be distinguished from a login with a copy of the key. There is no central point where it is logged when and for whom access was issued. The key is not issued; it is simply there.<\/p>&#13;\n&#13;\n<p class=\"wp-block-paragraph\">And finally, rotation: a long-lived key would have to be changed regularly. Generate a new key, roll it out on all servers, replace it in GitLab, remove the old one. In practice, however, this rarely happens, not least because a change affects all unknown users of the key, and only then does it become apparent where it was used. This brings us back to the question of who else is actually using the key.<\/p>&#13;\n&#13;\n<p class=\"wp-block-paragraph\">What we actually want is a rule like: &#8220;Only jobs from project <code>infra\/ansible<\/code> on the protected branch <code>main<\/code> receive access to the servers, only for the duration of the job and only from our runners.&#8221;<\/p>&#13;\n&#13;\n<h2 class=\"wp-block-heading\">Construction<\/h2>&#13;\n&#13;\n<p class=\"wp-block-paragraph\">To address the problems mentioned above, three components are used.<\/p>&#13;\n&#13;\n<p class=\"wp-block-paragraph\">GitLab issues a signed ID token to each CI job upon request. This is a JWT that contains, among other things, the project, the branch, and information on whether the branch is protected.<\/p>&#13;\n&#13;\n<p class=\"wp-block-paragraph\">HashiCorp Vault validates this token via the JWT authentication method. If the information in the token matches a predefined role, the job receives a Vault token. With this, it can have its public key signed via the SSH Secrets Engine, which acts as an SSH CA in Vault.<\/p>&#13;\n&#13;\n<p class=\"wp-block-paragraph\">The servers trust the SSH CA and only allow a dedicated user <code>ansible<\/code> with a certificate from this CA.<\/p>&#13;\n&#13;\n<p class=\"wp-block-paragraph\">The process of a job thus looks as follows:<\/p>&#13;\n&#13;\n<figure class=\"wp-block-image size-large\"><img decoding=\"async\" src=\"https:\/\/www.credativ.de\/wp-content\/uploads\/2026\/10\/diagram-1.png\" alt=\"\"\/><\/figure>&#13;\n&#13;\n<p class=\"wp-block-paragraph\">The private key is generated within the job and never leaves it. Vault only sees the public key, GitLab neither. No secret for server access is stored in GitLab itself. Whether a job receives access is decided by Vault based on the information in the ID token, not by the pipeline definition, which can ultimately be changed in any branch.<\/p>&#13;\n&#13;\n<h2 class=\"wp-block-heading\">Setup<\/h2>&#13;\n&#13;\n<p class=\"wp-block-paragraph\">For the following examples, we assume an existing GitLab instance with runners and Vault. The Community Edition of GitLab is sufficient. Vault must be able to reach GitLab&#8217;s OIDC discovery URL, and the runner jobs must be able to reach Vault. In addition, an audit device should be activated in Vault so that every login and signature is logged. We use <code>gitlab.example.com<\/code>, <code>vault.example.com<\/code>, the project <code>infra\/ansible<\/code> and the server user <code>ansible<\/code> as names.<\/p>&#13;\n&#13;\n<p class=\"wp-block-paragraph\">For clarity, we set up Vault here with the <code>vault<\/code> CLI. In a GitOps setup, it is of course advisable to maintain this configuration in the repository and deploy it via the pipeline with Ansible or Terraform\/OpenTofu.<\/p>&#13;\n&#13;\n<h3 class=\"wp-block-heading\">Connecting GitLab and Vault<\/h3>&#13;\n&#13;\n<p class=\"wp-block-paragraph\">GitLab acts as an OIDC provider for CI jobs. The ID tokens are signed with a GitLab key, and GitLab publishes the associated public keys under <code>\/.well-known\/openid-configuration<\/code>. Vault therefore does not need to share a secret with GitLab, but merely needs to know which issuer to trust:<\/p>&#13;\n\r\n<pre class=\"wp-block-code\"><code>$ vault auth enable -path=gitlab jwt\r\nSuccess! Enabled jwt auth method at: gitlab\/\r\n\r\n$ vault write auth\/gitlab\/config \\\r\n    oidc_discovery_url=https:\/\/gitlab.example.com \\\r\n    bound_issuer=https:\/\/gitlab.example.com\r\nSuccess! Data written to: auth\/gitlab\/config<\/code><\/pre>\r\n&#13;\n<p class=\"wp-block-paragraph\">Via <code>oidc_discovery_url<\/code>, Vault retrieves GitLab&#8217;s signing keys, <code>bound_issuer<\/code> ensures that only tokens from this exact issuer are accepted. The path <code>gitlab<\/code> is freely selectable. It will later appear in the login path and in the name of the Vault tokens.<\/p>&#13;\n&#13;\n<p class=\"wp-block-paragraph\">The content of the ID token becomes interesting. Decoded, the payload of a token for a job on <code>main<\/code> looks like this (abbreviated):<\/p>&#13;\n\r\n<pre class=\"wp-block-code\"><code>{\r\n  \"iss\": \"https:\/\/gitlab.example.com\",\r\n  \"aud\": \"https:\/\/vault.example.com\",\r\n  \"sub\": \"project_path:infra\/ansible:ref_type:branch:ref:main\",\r\n  \"iat\": 1760890441,\r\n  \"nbf\": 1760890436,\r\n  \"exp\": 1760894041,\r\n  \"project_path\": \"infra\/ansible\",\r\n  \"ref\": \"main\",\r\n  \"ref_type\": \"branch\",\r\n  \"ref_protected\": \"true\",\r\n  \"pipeline_source\": \"push\",\r\n  \"user_login\": \"jdoe\",\r\n  \"job_id\": \"4711\",\r\n  \"ci_config_ref_uri\": \"gitlab.example.com\/infra\/ansible\/\/.gitlab-ci.yml@refs\/heads\/main\"\r\n}<\/code><\/pre>\r\n&#13;\n<p class=\"wp-block-paragraph\">In addition to the standard claims <code>iss<\/code>, <code>aud<\/code>, <code>sub<\/code> and the timestamps, we find the interesting claims <code>project_path<\/code>, <code>ref<\/code>,  <code>ref_type<\/code>  and <code>ref_protected<\/code>. It is noticeable that <code>ref_protected<\/code> is transmitted as a string and not as a boolean. This is important later when configuring the Vault role. Furthermore, other claims such as <code>pipeline_source<\/code>, <code>user_login<\/code>,  <code>environment<\/code>  or <code>ci_config_ref_uri<\/code> are available.<\/p>&#13;\n&#13;\n<h3 class=\"wp-block-heading\">Setting up the SSH CA<\/h3>&#13;\n&#13;\n<p class=\"wp-block-paragraph\">Vault&#8217;s SSH Secrets Engine can act as a Certificate Authority. It signs public SSH keys and generates OpenSSH certificates from them. We let Vault itself generate the CA&#8217;s signing key, so the private part never leaves Vault:<\/p>&#13;\n\r\n<pre class=\"wp-block-code\"><code>$ vault secrets enable ssh\r\nSuccess! Enabled the ssh secrets engine at: ssh\/\r\n\r\n$ vault write ssh\/config\/ca generate_signing_key=true key_type=ssh-ed25519\r\nKey           Value\r\n---           -----\r\npublic_key    ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA...<\/code><\/pre>\r\n&#13;\n<p class=\"wp-block-paragraph\">Vault also provides the CA&#8217;s public key without authentication. We will need it later on the servers:<\/p>&#13;\n\r\n<pre class=\"wp-block-code\"><code>$ curl https:\/\/vault.example.com\/v1\/ssh\/public_key\r\nssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA...<\/code><\/pre>\r\n&#13;\n<p class=\"wp-block-paragraph\">We define which certificates the CA issues via a role:<\/p>&#13;\n\r\n<pre class=\"wp-block-code\"><code>$ vault write ssh\/roles\/ansible - &lt;&lt;'EOF'\r\n{\r\n  \"key_type\": \"ca\",\r\n  \"allow_user_certificates\": true,\r\n  \"allowed_users\": \"ansible\",\r\n  \"default_user\": \"ansible\",\r\n  \"allowed_extensions\": \"permit-pty\",\r\n  \"default_extensions\": {\"permit-pty\": \"\"},\r\n  \"ttl\": \"30m\",\r\n  \"max_ttl\": \"1h\"\r\n}\r\nEOF\r\nSuccess! Data written to: ssh\/roles\/ansible<\/code><\/pre>\r\n&#13;\n<p class=\"wp-block-paragraph\">The role is structured as follows: it issues only user certificates, and only for the principal <code>ansible<\/code>. Only <code>permit-pty<\/code> is set as an extension. Port, agent, and X11 forwarding are not possible with this in the certificate. A certificate is valid for 30 minutes by default, and a maximum of one hour upon request. If a client tries to deviate from this, Vault rejects the request:<\/p>&#13;\n\r\n<pre class=\"wp-block-code\"><code>$ vault write ssh\/sign\/ansible public_key=@id_ed25519.pub valid_principals=root\r\n\u2026\r\n* root is not a valid value for valid_principals\r\n\r\n$ vault write ssh\/sign\/ansible public_key=@id_ed25519.pub ttl=2h\r\n\u2026\r\n* ttl is larger than maximum allowed 3600<\/code><\/pre>\r\n&#13;\n<p class=\"wp-block-paragraph\">Optionally, the Key ID in the certificate can be influenced via <code>key_id_format<\/code>. Without further configuration, it contains a hash of the Vault token (<code>vault-token-&lt;hash&gt;<\/code>). With <code>key_id_format='{{token_display_name}}'<\/code>, after a login via our JWT authentication method, it will instead show <code>gitlab-infra\/ansible<\/code>. Since <code>sshd<\/code> logs the Key ID with every login, this is significantly more informative in the log.<\/p>&#13;\n&#13;\n<p class=\"wp-block-paragraph\">Additionally, the certificate can be bound to the source IP. Via the Critical Option <code>source-address<\/code>, <code>sshd<\/code> accepts the certificate only from the specified addresses, in our case those of the runners. A compromised certificate is thus worthless outside this network. For this, we add the following entries to the role:<\/p>&#13;\n\r\n<pre class=\"wp-block-code\"><code>  \"default_critical_options\": {\"source-address\": \"192.0.2.0\/24\"},\r\n  \"allowed_critical_options\": \"verify-required\"<\/code><\/pre>\r\n&#13;\n<p class=\"wp-block-paragraph\">The second entry seems a bit strange at first glance, but it is important. <code>allowed_critical_options<\/code> defines which Critical Options a client is allowed to set in its signature request. An empty list does not mean &#8220;none&#8221;, but &#8220;all&#8221;.<\/p>&#13;\n&#13;\n<p class=\"wp-block-paragraph\">This implies two things: <code>source-address<\/code> must under no circumstances be in this list, otherwise the client could overwrite the default value in its request, for example with <code>0.0.0.0\/0<\/code>. However, the list must not remain empty either. So we need to find another harmless option that we allow as the only one, so to speak, as a placeholder. <code>verify-required<\/code> is suitable for this: the option requires user confirmation for FIDO keys, for example via PIN, and can at most further restrict a certificate.<\/p>&#13;\n&#13;\n<p class=\"wp-block-paragraph\">If a client now tries to set <code>source-address<\/code> itself, Vault rejects the request with <code>critical options not on allowed list: [source-address]<\/code>. Vault still sets the default value from <code>default_critical_options<\/code>. A login from a different address is acknowledged by <code>sshd<\/code> with <code>Refused by certificate options<\/code>.<\/p>&#13;\n&#13;\n<h3 class=\"wp-block-heading\">Policies and Roles<\/h3>&#13;\n&#13;\n<p class=\"wp-block-paragraph\">Who is allowed to do what is governed by two things in Vault: a role of the JWT authentication method defines which jobs are allowed to log in and which policies they receive. The policy, in turn, determines what the issued Vault token is allowed to do. For runs against the servers, the right to sign via our SSH role is sufficient:<\/p>&#13;\n\r\n<pre class=\"wp-block-code\"><code>$ cat ansible-apply.hcl\r\npath \"ssh\/sign\/ansible\" {\r\n  capabilities = &#091;\"update\"]\r\n}\r\npath \"kv\/data\/ansible\/*\" {\r\n  capabilities = &#091;\"read\"]\r\n}\r\n\r\n$ vault policy write ansible-apply ansible-apply.hcl\r\nSuccess! Uploaded policy: ansible-apply<\/code><\/pre>\r\n&#13;\n<p class=\"wp-block-paragraph\">The second block additionally allows reading secrets for the playbooks from the KV engine. The same login could therefore also provide further secrets.<\/p>&#13;\n&#13;\n<p class=\"wp-block-paragraph\">More interesting is the role, because here we bind the login to the project and branch:<\/p>&#13;\n\r\n<pre class=\"wp-block-code\"><code>$ vault write auth\/gitlab\/role\/ansible-apply - &lt;&lt;'EOF'\r\n{\r\n  \"role_type\": \"jwt\",\r\n  \"user_claim\": \"project_path\",\r\n  \"bound_audiences\": &#091;\"https:\/\/vault.example.com\"],\r\n  \"bound_claims_type\": \"string\",\r\n  \"bound_claims\": {\r\n    \"project_path\": \"infra\/ansible\",\r\n    \"ref\": \"main\",\r\n    \"ref_type\": \"branch\",\r\n    \"ref_protected\": \"true\"\r\n  },\r\n  \"token_policies\": &#091;\"ansible-apply\"],\r\n  \"token_ttl\": \"1h\",\r\n  \"token_max_ttl\": \"1h\"\r\n}\r\nEOF\r\nSuccess! Data written to: auth\/gitlab\/role\/ansible-apply<\/code><\/pre>\r\n&#13;\n<p class=\"wp-block-paragraph\"><code>bound_audiences<\/code>  defines for which recipient a token must be issued. The job determines the value when requesting the ID token via the parameter <code>aud<\/code> under <code>id_tokens<\/code> in the <code>.gitlab-ci.yml<\/code> (see section &#8220;Pipeline&#8221;), and GitLab adopts it as claim <code>aud<\/code> in the token. Usually, this is the URL of the service that validates the token, here <code>https:\/\/vault.example.com<\/code>. Vault only accepts tokens whose  <code>aud<\/code>  matches one of the values in <code>bound_audiences<\/code>. A token that a job requested for another service cannot therefore be used with Vault. The actual conditions are in <code>bound_claims<\/code>, where all must apply. A job from a feature branch or from an unprotected <code>main<\/code> will accordingly fail during login:<\/p>&#13;\n\r\n<pre class=\"wp-block-code\"><code>* error validating claims: claim \"ref\" does not match any associated bound claim values\r\n* error validating claims: claim \"ref_protected\" does not match any associated bound claim values<\/code><\/pre>\r\n&#13;\n<p class=\"wp-block-paragraph\">Especially <code>ref_protected<\/code> is crucial here. Since GitLab transmits the value as a string, it also appears as <code>\"true\"<\/code> in quotation marks in the role. Only then does GitLab&#8217;s branch protection, i.e., the question of who is allowed to push and merge to <code>main<\/code>, also apply to server access. If more flexible rules are needed, <code>bound_claims_type=glob<\/code> can also be used for patterns like <code>release\/*<\/code> or additional claims such as  <code>pipeline_source<\/code>  can be added.<\/p>&#13;\n&#13;\n<p class=\"wp-block-paragraph\">The source IP can also be restricted during login. With <code>\"token_bound_cidrs\": [\"192.0.2.0\/24\"]<\/code> in the role, Vault only accepts logins from the runner network (otherwise <code>permission denied<\/code>), and the issued token is also only usable from there. If Vault is behind a load balancer or reverse proxy, Vault must know the real client address, for example via <code>x_forwarded_for_authorized_addrs<\/code> in the listener.<\/p>&#13;\n&#13;\n<p class=\"wp-block-paragraph\">In practice, a gradation into several roles has proven effective. Merge requests receive read-only access via a role <code>ansible-preview<\/code>, which is only bound to <code>project_path<\/code>, but no SSH certificates. The roles <code>ansible-check<\/code> for the check run and <code>ansible-apply<\/code> for the actual deployment are both bound to the protected <code>main<\/code> and differ only in their policies.<\/p>&#13;\n&#13;\n<p class=\"wp-block-paragraph\">One thing should be clear: even Ansible&#8217;s check mode logs in to the servers and uses <code>sudo<\/code>. There is no such thing as &#8220;read-only&#8221; SSH access. Any role that allows <code>ssh\/sign\/ansible<\/code> effectively means root access and must be strictly bound accordingly.<\/p>&#13;\n&#13;\n<h3 class=\"wp-block-heading\">Preparing the Server<\/h3>&#13;\n&#13;\n<p class=\"wp-block-paragraph\">Three things are needed on the servers, which can sensibly be rolled out with Ansible itself. First, we store the public key of the CA in <code>\/etc\/ssh\/trusted_user_ca_keys<\/code> and add the <code>sshd_config<\/code>:<\/p>&#13;\n\r\n<pre class=\"wp-block-code\"><code>TrustedUserCAKeys \/etc\/ssh\/trusted_user_ca_keys\r\nAuthorizedPrincipalsFile \/etc\/ssh\/auth_principals\/%u\r\nAllowUsers admin ansible<\/code><\/pre>\r\n&#13;\n<p class=\"wp-block-paragraph\"><code>TrustedUserCAKeys<\/code>  allows certificates from this CA. <code>AuthorizedPrincipalsFile<\/code> is also important here: without this option, <code>sshd<\/code> accepts a certificate for any local user whose name is listed as a principal in the certificate. With this option, each user requires their own file with the principals allowed for them. Users without this file cannot log in via certificate.<\/p>&#13;\n&#13;\n<p class=\"wp-block-paragraph\">Then we create the user <code>ansible<\/code> as a system user without a password and without <code>authorized_keys<\/code>, and give him a principals file and <code>sudo<\/code> rights:<\/p>&#13;\n\r\n<pre class=\"wp-block-code\"><code># cat \/etc\/ssh\/auth_principals\/ansible\r\nansible\r\n\r\n# cat \/etc\/sudoers.d\/ansible\r\nansible ALL=(ALL:ALL) NOPASSWD:ALL<\/code><\/pre>\r\n&#13;\n<p class=\"wp-block-paragraph\">A login as <code>ansible<\/code> is thus only possible with a valid certificate from our CA. <code>sshd<\/code> logs the Key ID, serial number, and CA of the certificate (here with <code>key_id_format<\/code> as described above):<\/p>&#13;\n\r\n<pre class=\"wp-block-code\"><code>Accepted publickey for ansible from 192.0.2.10 port 51945 ssh2: ED25519-CERT SHA256:\u2026 ID gitlab-infra\/ansible (serial 17651805833285797696) CA ED25519 SHA256:\u2026<\/code><\/pre>\r\n&#13;\n<p class=\"wp-block-paragraph\">If the principal is missing, the certificate is rejected with <code>Certificate does not contain an authorized principal<\/code>. To revoke a pipeline&#8217;s access to a server, it is therefore sufficient to remove the principals file there.<\/p>&#13;\n&#13;\n<p class=\"wp-block-paragraph\">There is one catch: a new server does not yet know the CA. The first run, which rolls out the CA key, user, and <code>sshd<\/code> configuration, therefore requires a different access, for example, an admin user with their own key.<\/p>&#13;\n&#13;\n<h3 class=\"wp-block-heading\">Pipeline<\/h3>&#13;\n&#13;\n<p class=\"wp-block-paragraph\">In the <code>.gitlab-ci.yml<\/code>, we combine the necessary steps into a template from which all jobs that run against the servers inherit:<\/p>&#13;\n\r\n<pre class=\"wp-block-code\"><code>.ansible-run:\r\n  id_tokens:\r\n    VAULT_ID_TOKEN:\r\n      aud: https:\/\/vault.example.com\r\n  variables:\r\n    VAULT_ADDR: https:\/\/vault.example.com\r\n    ANSIBLE_HOST_KEY_CHECKING: \"true\"\r\n    ANSIBLE_SSH_ARGS: &gt;-\r\n      -o ControlMaster=auto -o ControlPersist=60s\r\n      -o UserKnownHostsFile=$CI_PROJECT_DIR\/ci\/known_hosts\r\n      -o StrictHostKeyChecking=yes\r\n  before_script:\r\n    - export VAULT_TOKEN=\"$(vault write -field=token auth\/gitlab\/login role=\"$VAULT_ROLE\" jwt=\"$VAULT_ID_TOKEN\")\"\r\n    - mkdir -p -m 700 ~\/.ssh\r\n    - ssh-keygen -q -t ed25519 -N \"\" -f ~\/.ssh\/id_ed25519\r\n    - vault write -field=signed_key ssh\/sign\/ansible public_key=@\"$HOME\/.ssh\/id_ed25519.pub\" &gt; ~\/.ssh\/id_ed25519-cert.pub<\/code><\/pre>\r\n&#13;\n<p class=\"wp-block-paragraph\">Via <code>id_tokens<\/code>, the job requests an ID token with our audience from GitLab, which is then available in the variable <code>VAULT_ID_TOKEN<\/code>. With this, the job logs in to Vault, generates a new key pair, and has the public key signed. We store the certificate as <code>id_ed25519-cert.pub<\/code> next to the key, where OpenSSH finds it without further configuration. A look at the certificate shows the result:<\/p>&#13;\n\r\n<pre class=\"wp-block-code\"><code>$ ssh-keygen -L -f ~\/.ssh\/id_ed25519-cert.pub\r\n\/root\/.ssh\/id_ed25519-cert.pub:\r\n        Type: ssh-ed25519-cert-v01@openssh.com user certificate\r\n        Public key: ED25519-CERT SHA256:BSJp788v7wl\/esKJl6i95GRWmCa\/fEvKVJCJ\/Wbrc\/c\r\n        Signing CA: ED25519 SHA256:\/6RfVwCFNt+8ljufeokhOH\/ZLq+USJn2DarRocTfg2s (using ssh-ed25519)\r\n        Key ID: \"vault-token-052269efcf2fef097f7ac28997a8bde464569826bf7c4bca549089fd66eb73f7\"\r\n        Serial: 17651805833285797696\r\n        Valid: from 2026-10-19T18:34:41 to 2026-10-19T19:05:11\r\n        Principals:\r\n                ansible\r\n        Critical Options: (none)\r\n        Extensions:\r\n                permit-pty<\/code><\/pre>\r\n&#13;\n<p class=\"wp-block-paragraph\">Equally important is the opposite direction: the job should also not blindly trust its counterparts. We therefore store the host keys of the servers in <code>ci\/known_hosts<\/code> in the repository and enforce their verification with <code>StrictHostKeyChecking=yes<\/code>. The TLS connection to Vault is checked against the CA of the Vault certificate, and for an internal CA via <code>VAULT_CACERT<\/code>.<\/p>&#13;\n&#13;\n<p class=\"wp-block-paragraph\">The actual jobs then only differ in the Vault role and their <code>rules<\/code>:<\/p>&#13;\n\r\n<pre class=\"wp-block-code\"><code>preview:                # Merge Requests: nur Vorschau, kein Serverzugang\r\n  extends: .ansible-run\r\n  variables: {VAULT_ROLE: ansible-preview}\r\n  rules:\r\n    - if: $CI_PIPELINE_SOURCE == \"merge_request_event\"\r\n\r\ncheck:                  # main und n\u00e4chtlicher Zeitplan: --check --diff\r\n  extends: .ansible-run\r\n  variables: {VAULT_ROLE: ansible-check}\r\n  rules:\r\n    - if: $CI_PIPELINE_SOURCE == \"schedule\"\r\n    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH\r\n\r\napply:                  # main, manuell gestartet\r\n  extends: .ansible-run\r\n  variables: {VAULT_ROLE: ansible-apply}\r\n  resource_group: production\r\n  rules:\r\n    - if: $CI_PIPELINE_SOURCE == \"push\" &amp;&amp; $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH\r\n      when: manual<\/code><\/pre>\r\n&#13;\n<p class=\"wp-block-paragraph\">In the <code>preview<\/code> job, key generation and signature are omitted; the policy would deny them anyway. The <code>script<\/code> sections with the actual <code>ansible-playbook<\/code> calls have been omitted for clarity. <code>resource_group<\/code> ensures that no two apply runs work against the servers simultaneously.<\/p>&#13;\n&#13;\n<p class=\"wp-block-paragraph\">At this point, a closer look at the role of the <code>rules<\/code> is worthwhile. They merely control when a job is generated at all. The actual security boundary lies in Vault: if someone in a feature branch changes the pipeline definition so that a job requests the role <code>ansible-apply<\/code>, the login will fail due to the <code>bound_claims<\/code>.<\/p>&#13;\n&#13;\n<h3 class=\"wp-block-heading\">Overall picture<\/h3>&#13;\n&#13;\n<p class=\"wp-block-paragraph\">If we summarize the individual parts, a chain of trust emerges in which each link verifies the previous one. GitLab signs the ID token, Vault verifies the signature, issuer, audience, validity, and claims. Only if all conditions are met does the job receive a Vault token with a policy that allows signing. The SSH CA signs the key with a fixed principal, limited validity, and restricted extensions. <code>sshd<\/code> finally verifies the certificate against the stored CA and the principals file, <code>AllowUsers<\/code> and <code>sudoers<\/code> handle the rest.<\/p>&#13;\n&#13;\n<figure class=\"wp-block-image size-large\"><img decoding=\"async\" src=\"https:\/\/www.credativ.de\/wp-content\/uploads\/2026\/10\/diagram-2.png\" alt=\"\"\/><\/figure>&#13;\n&#13;\n<p class=\"wp-block-paragraph\">Who receives access is thus determined by branch protection in GitLab and the <code>bound_claims<\/code> in Vault. What the access allows is governed by policy, SSH role, principals file, and <code>sudo<\/code>. How long it is valid is determined by the TTLs of the Vault token and certificate. Logging occurs at three points: in Vault&#8217;s audit log, in the job log, and in the log of <code>sshd<\/code> on the server.<\/p>&#13;\n&#13;\n<h2 class=\"wp-block-heading\">Conclusion<\/h2>&#13;\n&#13;\n<p class=\"wp-block-paragraph\">As shown, the static deploy key can be replaced with short-lived certificates with manageable effort: a JWT authentication method, an SSH CA with a suitable role, some policies, a few lines in the <code>sshd_config<\/code> and a few lines in the pipeline.<\/p>&#13;\n&#13;\n<p class=\"wp-block-paragraph\">The advantages are obvious. There is no longer a long-lived secret in GitLab that could be lost or would need to be rotated. Each job generates its own key, and a certificate becomes worthless after 30 minutes at the latest. Forgotten copies of a key that are still in use somewhere can no longer exist. Access is tied to verifiable properties of the job: feature branches and merge requests do not receive a certificate, even with a modified pipeline definition. Via Vault&#8217;s audit log and the serial number and Key ID in the server logs, each login can be assigned to a job. Through roles and policies, it is also possible to finely differentiate who only receives a preview and who is allowed to deploy. And last but not least, the entire configuration can be maintained as code in the repository.<\/p>&#13;\n&#13;\n<p class=\"wp-block-paragraph\">Unfortunately, there are also disadvantages. If Vault cannot be reached or is unable to sign SSH keys (sealed, etc.), the pipeline will also not run. A second access for emergencies therefore remains necessary. In addition, the security boundary shifts. Anyone who receives signatures from the SSH CA has root on all servers. This therefore applies to anyone who is allowed to push or merge to <code>main<\/code>. Branch protection in GitLab and the policies in Vault thus become security-critical.<\/p>&#13;\n&#13;\n<p class=\"wp-block-paragraph\">Individual certificates cannot be revoked without an additional Key Revocation List, but the short validity period limits the risk. This validity period, conversely, also limits the duration of a run, because <code>sshd<\/code> checks the certificate with every new connection establishment, for example after a reboot. If the apply job is also allowed to write the Vault configuration itself, its Vault token is effectively an admin access to Vault for the duration of the job. In addition, there is the bootstrapping of new servers, the maintenance of host keys in the repository, and simply more moving parts. Errors often manifest only as <code>Permission denied (publickey)<\/code>; here, <code>ssh-keygen -L<\/code> in the job and the log of <code>sshd<\/code> on the server help.<\/p>&#13;\n&#13;\n<p class=\"wp-block-paragraph\">There is also potential for expansion: host certificates from Vault could replace the pinned host keys.<\/p>&#13;\n&#13;\n<p class=\"wp-block-paragraph\">All in all, for us the advantages clearly outweigh the disadvantages. Instead of a key that was created at some point and has been valid everywhere ever since, there is access that is generated per job in a traceable way and expires on its own.<\/p>&#13;\n&#13;\n<p class=\"wp-block-paragraph\">Everything we have shown in this post with Vault applies equally to OpenBao; the CLI there is simply called <code>bao<\/code>. The open-source fork of Vault is being developed under the umbrella of the OpenSSF of the Linux Foundation.<\/p>&#13;\n&#13;\n<p class=\"wp-block-paragraph\">If you need support using Ansible, GitLab, or Vault, our Open Source Support Center is available to you.<\/p>&#13;\n","protected":false},"excerpt":{"rendered":"<p>&#13; Anyone who manages their servers with Ansible will eventually face the question of whether the playbooks can also run automatically from a CI\/CD pipeline. If the Ansible repository is already in GitLab, GitLab CI is ideal for this: repository, merge requests, and pipeline are all in one place. This applies particularly to a GitOps [&hellip;]<\/p>\n","protected":false},"author":7,"featured_media":46038,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[2054,2053],"tags":[1710,1734],"class_list":["post-46033","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-ansible-en","category-automation","tag-ansible-en","tag-gitlab-en"],"acf":[],"yoast_head":"<!-- This site is optimized with the Yoast SEO Premium plugin v28.8 (Yoast SEO v28.8) - https:\/\/yoast.com\/product\/yoast-seo-premium-wordpress\/ -->\n<title>Temporary Root: Ansible in GitLab CI without Static SSH Keys - credativ\u00ae<\/title>\n<meta name=\"description\" content=\"Optimize your infrastructure management with Ansible gitlab ci. Secure access without static SSH keys is possible.\" \/>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/www.credativ.de\/en\/blog\/automation\/temporary-root-ansible-in-gitlab-ci-without-static-ssh-keys\/\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"Temporary Root: Ansible in GitLab CI without Static SSH Keys\" \/>\n<meta property=\"og:description\" content=\"Optimize your infrastructure management with Ansible gitlab ci. Secure access without static SSH keys is possible.\" \/>\n<meta property=\"og:url\" content=\"https:\/\/www.credativ.de\/en\/blog\/automation\/temporary-root-ansible-in-gitlab-ci-without-static-ssh-keys\/\" \/>\n<meta property=\"og:site_name\" content=\"credativ\u00ae\" \/>\n<meta property=\"article:publisher\" content=\"https:\/\/www.facebook.com\/credativDE\/\" \/>\n<meta property=\"article:published_time\" content=\"2026-11-03T11:11:54+00:00\" \/>\n<meta property=\"article:modified_time\" content=\"2026-11-03T11:16:05+00:00\" \/>\n<meta property=\"og:image\" content=\"https:\/\/www.credativ.de\/wp-content\/uploads\/2026\/11\/messingschluessel-serverraum-rechenzentrum-schloss.webp\" \/>\n\t<meta property=\"og:image:width\" content=\"1024\" \/>\n\t<meta property=\"og:image:height\" content=\"768\" \/>\n\t<meta property=\"og:image:type\" content=\"image\/webp\" \/>\n<meta name=\"author\" content=\"Adrian Vondendriesch\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:creator\" content=\"@credativde\" \/>\n<meta name=\"twitter:site\" content=\"@credativde\" \/>\n<meta name=\"twitter:label1\" content=\"Written by\" \/>\n\t<meta name=\"twitter:data1\" content=\"Adrian Vondendriesch\" \/>\n\t<meta name=\"twitter:label2\" content=\"Est. reading time\" \/>\n\t<meta name=\"twitter:data2\" content=\"31 minutes\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\\\/\\\/schema.org\",\"@graph\":[{\"@type\":\"Article\",\"@id\":\"https:\\\/\\\/www.credativ.de\\\/en\\\/blog\\\/automation\\\/temporary-root-ansible-in-gitlab-ci-without-static-ssh-keys\\\/#article\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/www.credativ.de\\\/en\\\/blog\\\/automation\\\/temporary-root-ansible-in-gitlab-ci-without-static-ssh-keys\\\/\"},\"author\":{\"name\":\"Adrian Vondendriesch\",\"@id\":\"https:\\\/\\\/www.credativ.de\\\/en\\\/#\\\/schema\\\/person\\\/a34b146f598af43f8845ea7e1f0b8ac4\"},\"headline\":\"Temporary Root: Ansible in GitLab CI without Static SSH Keys\",\"datePublished\":\"2026-11-03T11:11:54+00:00\",\"dateModified\":\"2026-11-03T11:16:05+00:00\",\"mainEntityOfPage\":{\"@id\":\"https:\\\/\\\/www.credativ.de\\\/en\\\/blog\\\/automation\\\/temporary-root-ansible-in-gitlab-ci-without-static-ssh-keys\\\/\"},\"wordCount\":2943,\"commentCount\":0,\"publisher\":{\"@id\":\"https:\\\/\\\/www.credativ.de\\\/en\\\/#organization\"},\"image\":{\"@id\":\"https:\\\/\\\/www.credativ.de\\\/en\\\/blog\\\/automation\\\/temporary-root-ansible-in-gitlab-ci-without-static-ssh-keys\\\/#primaryimage\"},\"thumbnailUrl\":\"https:\\\/\\\/www.credativ.de\\\/wp-content\\\/uploads\\\/2026\\\/11\\\/messingschluessel-serverraum-rechenzentrum-schloss.webp\",\"keywords\":[\"Ansible\",\"Gitlab\"],\"articleSection\":[\"Ansible\",\"Automation\"],\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"CommentAction\",\"name\":\"Comment\",\"target\":[\"https:\\\/\\\/www.credativ.de\\\/en\\\/blog\\\/automation\\\/temporary-root-ansible-in-gitlab-ci-without-static-ssh-keys\\\/#respond\"]}],\"copyrightYear\":\"2026\",\"copyrightHolder\":{\"@id\":\"https:\\\/\\\/www.credativ.de\\\/#organization\"}},{\"@type\":\"WebPage\",\"@id\":\"https:\\\/\\\/www.credativ.de\\\/en\\\/blog\\\/automation\\\/temporary-root-ansible-in-gitlab-ci-without-static-ssh-keys\\\/\",\"url\":\"https:\\\/\\\/www.credativ.de\\\/en\\\/blog\\\/automation\\\/temporary-root-ansible-in-gitlab-ci-without-static-ssh-keys\\\/\",\"name\":\"Temporary Root: Ansible in GitLab CI without Static SSH Keys - credativ\u00ae\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/www.credativ.de\\\/en\\\/#website\"},\"primaryImageOfPage\":{\"@id\":\"https:\\\/\\\/www.credativ.de\\\/en\\\/blog\\\/automation\\\/temporary-root-ansible-in-gitlab-ci-without-static-ssh-keys\\\/#primaryimage\"},\"image\":{\"@id\":\"https:\\\/\\\/www.credativ.de\\\/en\\\/blog\\\/automation\\\/temporary-root-ansible-in-gitlab-ci-without-static-ssh-keys\\\/#primaryimage\"},\"thumbnailUrl\":\"https:\\\/\\\/www.credativ.de\\\/wp-content\\\/uploads\\\/2026\\\/11\\\/messingschluessel-serverraum-rechenzentrum-schloss.webp\",\"datePublished\":\"2026-11-03T11:11:54+00:00\",\"dateModified\":\"2026-11-03T11:16:05+00:00\",\"description\":\"Optimize your infrastructure management with Ansible gitlab ci. Secure access without static SSH keys is possible.\",\"breadcrumb\":{\"@id\":\"https:\\\/\\\/www.credativ.de\\\/en\\\/blog\\\/automation\\\/temporary-root-ansible-in-gitlab-ci-without-static-ssh-keys\\\/#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\\\/\\\/www.credativ.de\\\/en\\\/blog\\\/automation\\\/temporary-root-ansible-in-gitlab-ci-without-static-ssh-keys\\\/\"]}]},{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\\\/\\\/www.credativ.de\\\/en\\\/blog\\\/automation\\\/temporary-root-ansible-in-gitlab-ci-without-static-ssh-keys\\\/#primaryimage\",\"url\":\"https:\\\/\\\/www.credativ.de\\\/wp-content\\\/uploads\\\/2026\\\/11\\\/messingschluessel-serverraum-rechenzentrum-schloss.webp\",\"contentUrl\":\"https:\\\/\\\/www.credativ.de\\\/wp-content\\\/uploads\\\/2026\\\/11\\\/messingschluessel-serverraum-rechenzentrum-schloss.webp\",\"width\":1024,\"height\":768,\"caption\":\"Brass key dissolves into glowing amber particles as it slides into the lock of a server cabinet door\"},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\\\/\\\/www.credativ.de\\\/en\\\/blog\\\/automation\\\/temporary-root-ansible-in-gitlab-ci-without-static-ssh-keys\\\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\\\/\\\/www.credativ.de\\\/en\\\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Temporary Root: Ansible in GitLab CI without Static SSH Keys\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\\\/\\\/www.credativ.de\\\/en\\\/#website\",\"url\":\"https:\\\/\\\/www.credativ.de\\\/en\\\/\",\"name\":\"credativ GmbH\",\"description\":\"\",\"publisher\":{\"@id\":\"https:\\\/\\\/www.credativ.de\\\/en\\\/#organization\"},\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\\\/\\\/www.credativ.de\\\/en\\\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"en-US\"},{\"@type\":[\"Organization\",\"Place\"],\"@id\":\"https:\\\/\\\/www.credativ.de\\\/en\\\/#organization\",\"name\":\"credativ\u00ae\",\"url\":\"https:\\\/\\\/www.credativ.de\\\/en\\\/\",\"logo\":{\"@id\":\"https:\\\/\\\/www.credativ.de\\\/en\\\/blog\\\/automation\\\/temporary-root-ansible-in-gitlab-ci-without-static-ssh-keys\\\/#local-main-organization-logo\"},\"image\":{\"@id\":\"https:\\\/\\\/www.credativ.de\\\/en\\\/blog\\\/automation\\\/temporary-root-ansible-in-gitlab-ci-without-static-ssh-keys\\\/#local-main-organization-logo\"},\"sameAs\":[\"https:\\\/\\\/www.facebook.com\\\/credativDE\\\/\",\"https:\\\/\\\/x.com\\\/credativde\",\"https:\\\/\\\/mastodon.social\\\/@credativde\",\"https:\\\/\\\/www.linkedin.com\\\/company\\\/credativ-gmbh\",\"https:\\\/\\\/www.instagram.com\\\/credativ\\\/\",\"https:\\\/\\\/www.northdata.de\\\/credativ GmbH, M\u00f6nchengladbach\\\/Amtsgericht Moenchengladbach HRB 23003\",\"https:\\\/\\\/github.com\\\/credativ\",\"https:\\\/\\\/www.youtube.com\\\/@credativGmbH\"],\"description\":\"credativ GmbH is an owner-managed, vendor-independent IT services and consulting company specializing in open source software, and one of the leading providers in this field. Founded in 1999, the company has been independently run by its management since March 2025 and is not part of any corporate group. We offer comprehensive, professional services ranging from consulting and infrastructure operations to 24\\\/7 support, custom solutions, and training. Our focus is on the holistic management of business-critical open source systems, including operating systems (e.g. Linux), databases (e.g. PostgreSQL), configuration management (e.g. Ansible, Puppet), and virtualization (e.g. Proxmox). As a committed member of the open source community, we help our customers use the benefits of free software securely, reliably, and efficiently in their IT environments.\",\"legalName\":\"credativ GmbH\",\"foundingDate\":\"1999-03-01\",\"duns\":\"316387060\",\"numberOfEmployees\":{\"@type\":\"QuantitativeValue\",\"minValue\":\"11\",\"maxValue\":\"50\"},\"address\":{\"@id\":\"https:\\\/\\\/www.credativ.de\\\/en\\\/blog\\\/automation\\\/temporary-root-ansible-in-gitlab-ci-without-static-ssh-keys\\\/#local-main-place-address\"},\"geo\":{\"@type\":\"GeoCoordinates\",\"latitude\":\"51.1732374\",\"longitude\":\"6.392010099999999\"},\"telephone\":[\"+4921619174200\",\"08002733284\"],\"contactPoint\":{\"@type\":\"ContactPoint\",\"telephone\":\"08002733284\",\"email\":\"vertrieb@credativ.de\"},\"openingHoursSpecification\":[{\"@type\":\"OpeningHoursSpecification\",\"dayOfWeek\":[\"Monday\",\"Tuesday\",\"Wednesday\",\"Thursday\",\"Friday\"],\"opens\":\"09:00\",\"closes\":\"17:00\"},{\"@type\":\"OpeningHoursSpecification\",\"dayOfWeek\":[\"Saturday\",\"Sunday\"],\"opens\":\"00:00\",\"closes\":\"00:00\"}],\"email\":\"info@credativ.de\",\"areaServed\":\"D-A-CH\",\"vatID\":\"DE452151696\"},{\"@type\":\"Person\",\"@id\":\"https:\\\/\\\/www.credativ.de\\\/en\\\/#\\\/schema\\\/person\\\/a34b146f598af43f8845ea7e1f0b8ac4\",\"name\":\"Adrian Vondendriesch\",\"description\":\"Adrian ist seit 2013 Mitarbeiter der credativ GmbH. Als technischer Leiter des Cloud Infrastructure Teams besch\u00e4ftigt er sich haupts\u00e4chlich mit der Planung, Realisierung und Betreuung verteilter Infrastrukturen wie zum Beispiel Kubernetes und Ceph sowie mit der Erarbeitung von Deployment-Strategien. Zuvor war er Teil des Datenbank-Teams bei credativ und war dort unter anderem mit dem Aufbau und der Verwaltung von hochverf\u00fcgbaren Datenbank-Systemen betreut. Seit 2015 beteiligt er sich aktiv am Debian-Projekt.\"},{\"@type\":\"PostalAddress\",\"@id\":\"https:\\\/\\\/www.credativ.de\\\/en\\\/blog\\\/automation\\\/temporary-root-ansible-in-gitlab-ci-without-static-ssh-keys\\\/#local-main-place-address\",\"streetAddress\":\"Hennes-Weisweiler-Allee 23\",\"addressLocality\":\"M\u00f6nchengladbach\",\"postalCode\":\"41179\",\"addressRegion\":\"Deutschland\",\"addressCountry\":\"DE\"},{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\\\/\\\/www.credativ.de\\\/en\\\/blog\\\/automation\\\/temporary-root-ansible-in-gitlab-ci-without-static-ssh-keys\\\/#local-main-organization-logo\",\"url\":\"https:\\\/\\\/www.credativ.de\\\/wp-content\\\/uploads\\\/2025\\\/04\\\/credativ-logo-right.svg\",\"contentUrl\":\"https:\\\/\\\/www.credativ.de\\\/wp-content\\\/uploads\\\/2025\\\/04\\\/credativ-logo-right.svg\",\"caption\":\"credativ\u00ae\"}]}<\/script>\n<meta name=\"geo.placename\" content=\"M\u00f6nchengladbach\" \/>\n<meta name=\"geo.position\" content=\"51.1732374;6.392010099999999\" \/>\n<meta name=\"geo.region\" content=\"Germany\" \/>\n<!-- \/ Yoast SEO Premium plugin. -->","yoast_head_json":{"title":"Temporary Root: Ansible in GitLab CI without Static SSH Keys - credativ\u00ae","description":"Optimize your infrastructure management with Ansible gitlab ci. Secure access without static SSH keys is possible.","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/www.credativ.de\/en\/blog\/automation\/temporary-root-ansible-in-gitlab-ci-without-static-ssh-keys\/","og_locale":"en_US","og_type":"article","og_title":"Temporary Root: Ansible in GitLab CI without Static SSH Keys","og_description":"Optimize your infrastructure management with Ansible gitlab ci. Secure access without static SSH keys is possible.","og_url":"https:\/\/www.credativ.de\/en\/blog\/automation\/temporary-root-ansible-in-gitlab-ci-without-static-ssh-keys\/","og_site_name":"credativ\u00ae","article_publisher":"https:\/\/www.facebook.com\/credativDE\/","article_published_time":"2026-11-03T11:11:54+00:00","article_modified_time":"2026-11-03T11:16:05+00:00","og_image":[{"width":1024,"height":768,"url":"https:\/\/www.credativ.de\/wp-content\/uploads\/2026\/11\/messingschluessel-serverraum-rechenzentrum-schloss.webp","type":"image\/webp"}],"author":"Adrian Vondendriesch","twitter_card":"summary_large_image","twitter_creator":"@credativde","twitter_site":"@credativde","twitter_misc":{"Written by":"Adrian Vondendriesch","Est. reading time":"31 minutes"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"Article","@id":"https:\/\/www.credativ.de\/en\/blog\/automation\/temporary-root-ansible-in-gitlab-ci-without-static-ssh-keys\/#article","isPartOf":{"@id":"https:\/\/www.credativ.de\/en\/blog\/automation\/temporary-root-ansible-in-gitlab-ci-without-static-ssh-keys\/"},"author":{"name":"Adrian Vondendriesch","@id":"https:\/\/www.credativ.de\/en\/#\/schema\/person\/a34b146f598af43f8845ea7e1f0b8ac4"},"headline":"Temporary Root: Ansible in GitLab CI without Static SSH Keys","datePublished":"2026-11-03T11:11:54+00:00","dateModified":"2026-11-03T11:16:05+00:00","mainEntityOfPage":{"@id":"https:\/\/www.credativ.de\/en\/blog\/automation\/temporary-root-ansible-in-gitlab-ci-without-static-ssh-keys\/"},"wordCount":2943,"commentCount":0,"publisher":{"@id":"https:\/\/www.credativ.de\/en\/#organization"},"image":{"@id":"https:\/\/www.credativ.de\/en\/blog\/automation\/temporary-root-ansible-in-gitlab-ci-without-static-ssh-keys\/#primaryimage"},"thumbnailUrl":"https:\/\/www.credativ.de\/wp-content\/uploads\/2026\/11\/messingschluessel-serverraum-rechenzentrum-schloss.webp","keywords":["Ansible","Gitlab"],"articleSection":["Ansible","Automation"],"inLanguage":"en-US","potentialAction":[{"@type":"CommentAction","name":"Comment","target":["https:\/\/www.credativ.de\/en\/blog\/automation\/temporary-root-ansible-in-gitlab-ci-without-static-ssh-keys\/#respond"]}],"copyrightYear":"2026","copyrightHolder":{"@id":"https:\/\/www.credativ.de\/#organization"}},{"@type":"WebPage","@id":"https:\/\/www.credativ.de\/en\/blog\/automation\/temporary-root-ansible-in-gitlab-ci-without-static-ssh-keys\/","url":"https:\/\/www.credativ.de\/en\/blog\/automation\/temporary-root-ansible-in-gitlab-ci-without-static-ssh-keys\/","name":"Temporary Root: Ansible in GitLab CI without Static SSH Keys - credativ\u00ae","isPartOf":{"@id":"https:\/\/www.credativ.de\/en\/#website"},"primaryImageOfPage":{"@id":"https:\/\/www.credativ.de\/en\/blog\/automation\/temporary-root-ansible-in-gitlab-ci-without-static-ssh-keys\/#primaryimage"},"image":{"@id":"https:\/\/www.credativ.de\/en\/blog\/automation\/temporary-root-ansible-in-gitlab-ci-without-static-ssh-keys\/#primaryimage"},"thumbnailUrl":"https:\/\/www.credativ.de\/wp-content\/uploads\/2026\/11\/messingschluessel-serverraum-rechenzentrum-schloss.webp","datePublished":"2026-11-03T11:11:54+00:00","dateModified":"2026-11-03T11:16:05+00:00","description":"Optimize your infrastructure management with Ansible gitlab ci. Secure access without static SSH keys is possible.","breadcrumb":{"@id":"https:\/\/www.credativ.de\/en\/blog\/automation\/temporary-root-ansible-in-gitlab-ci-without-static-ssh-keys\/#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/www.credativ.de\/en\/blog\/automation\/temporary-root-ansible-in-gitlab-ci-without-static-ssh-keys\/"]}]},{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/www.credativ.de\/en\/blog\/automation\/temporary-root-ansible-in-gitlab-ci-without-static-ssh-keys\/#primaryimage","url":"https:\/\/www.credativ.de\/wp-content\/uploads\/2026\/11\/messingschluessel-serverraum-rechenzentrum-schloss.webp","contentUrl":"https:\/\/www.credativ.de\/wp-content\/uploads\/2026\/11\/messingschluessel-serverraum-rechenzentrum-schloss.webp","width":1024,"height":768,"caption":"Brass key dissolves into glowing amber particles as it slides into the lock of a server cabinet door"},{"@type":"BreadcrumbList","@id":"https:\/\/www.credativ.de\/en\/blog\/automation\/temporary-root-ansible-in-gitlab-ci-without-static-ssh-keys\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/www.credativ.de\/en\/"},{"@type":"ListItem","position":2,"name":"Temporary Root: Ansible in GitLab CI without Static SSH Keys"}]},{"@type":"WebSite","@id":"https:\/\/www.credativ.de\/en\/#website","url":"https:\/\/www.credativ.de\/en\/","name":"credativ GmbH","description":"","publisher":{"@id":"https:\/\/www.credativ.de\/en\/#organization"},"potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/www.credativ.de\/en\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"en-US"},{"@type":["Organization","Place"],"@id":"https:\/\/www.credativ.de\/en\/#organization","name":"credativ\u00ae","url":"https:\/\/www.credativ.de\/en\/","logo":{"@id":"https:\/\/www.credativ.de\/en\/blog\/automation\/temporary-root-ansible-in-gitlab-ci-without-static-ssh-keys\/#local-main-organization-logo"},"image":{"@id":"https:\/\/www.credativ.de\/en\/blog\/automation\/temporary-root-ansible-in-gitlab-ci-without-static-ssh-keys\/#local-main-organization-logo"},"sameAs":["https:\/\/www.facebook.com\/credativDE\/","https:\/\/x.com\/credativde","https:\/\/mastodon.social\/@credativde","https:\/\/www.linkedin.com\/company\/credativ-gmbh","https:\/\/www.instagram.com\/credativ\/","https:\/\/www.northdata.de\/credativ GmbH, M\u00f6nchengladbach\/Amtsgericht Moenchengladbach HRB 23003","https:\/\/github.com\/credativ","https:\/\/www.youtube.com\/@credativGmbH"],"description":"credativ GmbH is an owner-managed, vendor-independent IT services and consulting company specializing in open source software, and one of the leading providers in this field. Founded in 1999, the company has been independently run by its management since March 2025 and is not part of any corporate group. We offer comprehensive, professional services ranging from consulting and infrastructure operations to 24\/7 support, custom solutions, and training. Our focus is on the holistic management of business-critical open source systems, including operating systems (e.g. Linux), databases (e.g. PostgreSQL), configuration management (e.g. Ansible, Puppet), and virtualization (e.g. Proxmox). As a committed member of the open source community, we help our customers use the benefits of free software securely, reliably, and efficiently in their IT environments.","legalName":"credativ GmbH","foundingDate":"1999-03-01","duns":"316387060","numberOfEmployees":{"@type":"QuantitativeValue","minValue":"11","maxValue":"50"},"address":{"@id":"https:\/\/www.credativ.de\/en\/blog\/automation\/temporary-root-ansible-in-gitlab-ci-without-static-ssh-keys\/#local-main-place-address"},"geo":{"@type":"GeoCoordinates","latitude":"51.1732374","longitude":"6.392010099999999"},"telephone":["+4921619174200","08002733284"],"contactPoint":{"@type":"ContactPoint","telephone":"08002733284","email":"vertrieb@credativ.de"},"openingHoursSpecification":[{"@type":"OpeningHoursSpecification","dayOfWeek":["Monday","Tuesday","Wednesday","Thursday","Friday"],"opens":"09:00","closes":"17:00"},{"@type":"OpeningHoursSpecification","dayOfWeek":["Saturday","Sunday"],"opens":"00:00","closes":"00:00"}],"email":"info@credativ.de","areaServed":"D-A-CH","vatID":"DE452151696"},{"@type":"Person","@id":"https:\/\/www.credativ.de\/en\/#\/schema\/person\/a34b146f598af43f8845ea7e1f0b8ac4","name":"Adrian Vondendriesch","description":"Adrian ist seit 2013 Mitarbeiter der credativ GmbH. Als technischer Leiter des Cloud Infrastructure Teams besch\u00e4ftigt er sich haupts\u00e4chlich mit der Planung, Realisierung und Betreuung verteilter Infrastrukturen wie zum Beispiel Kubernetes und Ceph sowie mit der Erarbeitung von Deployment-Strategien. Zuvor war er Teil des Datenbank-Teams bei credativ und war dort unter anderem mit dem Aufbau und der Verwaltung von hochverf\u00fcgbaren Datenbank-Systemen betreut. Seit 2015 beteiligt er sich aktiv am Debian-Projekt."},{"@type":"PostalAddress","@id":"https:\/\/www.credativ.de\/en\/blog\/automation\/temporary-root-ansible-in-gitlab-ci-without-static-ssh-keys\/#local-main-place-address","streetAddress":"Hennes-Weisweiler-Allee 23","addressLocality":"M\u00f6nchengladbach","postalCode":"41179","addressRegion":"Deutschland","addressCountry":"DE"},{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/www.credativ.de\/en\/blog\/automation\/temporary-root-ansible-in-gitlab-ci-without-static-ssh-keys\/#local-main-organization-logo","url":"https:\/\/www.credativ.de\/wp-content\/uploads\/2025\/04\/credativ-logo-right.svg","contentUrl":"https:\/\/www.credativ.de\/wp-content\/uploads\/2025\/04\/credativ-logo-right.svg","caption":"credativ\u00ae"}]},"geo.placename":"M\u00f6nchengladbach","geo.position":{"lat":"51.1732374","long":"6.392010099999999"},"geo.region":"Germany"},"permalink_manager":null,"_links":{"self":[{"href":"https:\/\/www.credativ.de\/en\/wp-json\/wp\/v2\/posts\/46033","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.credativ.de\/en\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.credativ.de\/en\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.credativ.de\/en\/wp-json\/wp\/v2\/users\/7"}],"replies":[{"embeddable":true,"href":"https:\/\/www.credativ.de\/en\/wp-json\/wp\/v2\/comments?post=46033"}],"version-history":[{"count":5,"href":"https:\/\/www.credativ.de\/en\/wp-json\/wp\/v2\/posts\/46033\/revisions"}],"predecessor-version":[{"id":46042,"href":"https:\/\/www.credativ.de\/en\/wp-json\/wp\/v2\/posts\/46033\/revisions\/46042"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/www.credativ.de\/en\/wp-json\/wp\/v2\/media\/46038"}],"wp:attachment":[{"href":"https:\/\/www.credativ.de\/en\/wp-json\/wp\/v2\/media?parent=46033"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.credativ.de\/en\/wp-json\/wp\/v2\/categories?post=46033"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.credativ.de\/en\/wp-json\/wp\/v2\/tags?post=46033"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}