Managing Composer Auth, Private Repositories and Mirrors Securely in GitLab
AI generated
CI/CD
.yml
GitLab · Composer · Magento · Private Repositories
Composer Auth, Private Repositories and Mirrors
Managed Securely and Reproducibly in GitLab

Anyone who stores Composer credentials in the repository risks compromise at every offboarding. Anyone who does not manage them at all ends up fighting broken builds. This article shows how to store auth.json, private repository tokens and Packagist mirror configurations securely as GitLab variables and consume them correctly in pipelines.

12 min read auth.json · COMPOSER_AUTH · Private Packagist · Mirror Composer 2.x · GitLab 16.x · Magento 2.4.x

1. The Problem with Composer Credentials in CI

Composer credentials, meaning access data for the Magento Marketplace, commercial extensions and private package sources, are in many projects either checked into the repository as auth.json or stored manually on the build server. Both approaches are problematic: checked in credentials show up in the Git history and are accessible with every clone of the repository, even to developers who left the team long ago. Manually stored credentials on build servers are invisible, rarely rotated and tied to the lifetime of the server.

GitLab CI/CD variables solve this problem structurally: the credentials live exclusively in GitLab, not in the repository and not on the server. They can be scoped per environment, restricted to authorized branches with the protected flag, and rotated at any time without touching the code or the server configuration. Anyone who follows this approach consistently ends up with a reproducible build process where every developer can run the same pipeline without needing to know any local credentials, and where offboarding requires no manual credential rotation.

2. auth.json: Format, Contents and Limitations

The auth.json file is Composer's primary way of storing authentication credentials. It supports three types: http-basic for username and password authentication (as used by the Magento Marketplace), bearer for token based authentication (as used by GitLab personal access tokens or GitHub tokens), and gitlab-token as a special alias for GitLab token auth. The file has a clear JSON structure with host names as keys, and Composer automatically finds the matching credentials for a given package source.

The limitations of auth.json: it is a local file that has to be placed on every developer machine or CI runner individually. In CI environments that means the file is either pre-placed on the runner (bad practice, since it cannot be versioned) or generated from a variable in every job script. Composer has a more elegant option for this: the environment variable COMPOSER_AUTH holds the same JSON content as the file, and Composer reads both with identical priority. This is the recommended approach for GitLab pipelines, because the variable is passed directly from GitLab without any file writing steps in the script.

# auth.json format, used as the COMPOSER_AUTH variable value in GitLab
# Set this entire JSON as the value of the COMPOSER_AUTH CI/CD Variable
{
  "http-basic": {
    "repo.magento.com": {
      "username": "public-key-from-marketplace",
      "password": "private-key-from-marketplace"
    },
    "hyva-themes.com": {
      "username": "token",
      "password": "your-hyva-license-key"
    }
  },
  "bearer": {
    "gitlab.example.com": "glpat-your-personal-access-token"
  }
}

3. Creating COMPOSER_AUTH as a GitLab Variable

The variable is created in GitLab under Settings → CI/CD → Variables. The value is the complete JSON content of auth.json: no file name, just the JSON string. Important configuration: set the masked flag so the JSON content does not appear in plain text in job logs. Set the protected flag if the value is only needed in production pipelines. The environment scope should be as narrow as possible: if staging needs different Composer credentials than production, two separate variables with the same name but different scope are the clean solution.

A common mistake when creating the variable: the JSON contains a line break error or incorrect quoting, which causes Composer to ignore the variable or abort with a JSON parse error. Before creating the variable it is worth validating the JSON content locally with jq . auth.json. Another pitfall: for very long Composer auth strings (many packages, many credentials) the masked feature can fail, because GitLab has a length limit for masked variables. In that case you either have to rely on the protected flag alone, or split the credentials across multiple variables.

4. Magento Marketplace: Public Key and Access Key

The Magento Marketplace uses http-basic authentication with two different keys: the Public Key corresponds to the username, the Access Key to the password. Both are generated in the marketplace account under Access Keys. The Public Key can be shared relatively freely, since it identifies the account but does not contain a secret. The Access Key is the actual secret and must be stored as a masked and protected variable.

For production pipelines it is recommended to create dedicated marketplace keys for CI, separate from personal developer keys. That way the CI key can be rotated or revoked at any time without disrupting the development workflow. Another benefit: the marketplace API limits requests per key. Parallel builds across multiple branches can run into rate limit errors if all builds use the same personal key. Dedicated CI keys have higher limits and are more clearly tied to the automated build process.

5. Private Repositories: GitLab, GitHub and Satis

In Magento projects, private Composer packages typically come from three sources: an own GitLab repository (custom module), a commercial vendor with a private GitHub repository, or a self hosted Satis server. Authentication for all three sources is configured through the COMPOSER_AUTH variable. GitLab repositories use either a personal access token with read_api scope or a deploy token that only has read access to the repository. Deploy tokens are the safer choice, because they are not tied to a user account and do not become invalid during offboarding.

The same principles apply to Satis servers that host Composer packages internally: basic auth with a dedicated CI user, the token in the COMPOSER_AUTH variable, no hardcoding in composer.json. Important: repository URLs in composer.json must not contain embedded credentials (so no https://user:password@satis.intern/). Credentials come exclusively from the variable. This makes it possible to check in composer.json without any trouble, without exposing credentials.

# Build job, consuming COMPOSER_AUTH from a GitLab CI/CD variable
build:composer:
  stage: build
  image: php:8.4-cli
  variables:
    # COMPOSER_CACHE_DIR enables GitLab cache for vendor packages
    COMPOSER_CACHE_DIR: ".cache/composer"
    # COMPOSER_AUTH is injected automatically from GitLab Variable
    # No need to write auth.json manually
  cache:
    key: "composer-$CI_COMMIT_REF_SLUG"
    paths:
      - .cache/composer/
    policy: pull-push
  before_script:
    - apt-get update -qq && apt-get install -y -qq git unzip
    - php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');"
    - php composer-setup.php --quiet
  script:
    # Composer automatically reads COMPOSER_AUTH environment variable
    - php composer.phar install --no-dev --prefer-dist --no-interaction --no-ansi
    - php composer.phar dump-autoload --optimize --no-dev
    # Verify no auth.json was accidentally written
    - test ! -f auth.json || { echo "ERROR: auth.json must not exist"; exit 1; }
  artifacts:
    paths:
      - vendor/
    expire_in: 2 hours

6. Configuring the Composer Mirror and Private Packagist

Composer mirrors and Private Packagist are used in projects to decouple dependencies from the public Packagist infrastructure, reduce download times and cache packages internally. A mirror is configured either through the repositories block in composer.json or through composer config commands in the build script. Private Packagist specifically requires a token that is passed through the COMPOSER_AUTH variable, in the same JSON format as other bearer token sources.

Important for mirror configurations: the mirror must be reachable from the build job. If the GitLab runner runs on an internal network and the mirror is hosted internally, that is not a problem. If the runner runs in the cloud and the mirror is internal, you need a VPN or an external mirror. Configuring the mirror in composer.json itself is unproblematic, since it contains no credentials. Authentication against the mirror comes from the variable. Following this separation between configuration (in the repository) and credentials (in GitLab) consistently is the central principle of a secure Composer setup.

7. Consuming Composer Auth Correctly in the Pipeline

In the pipeline the rule is: Composer reads COMPOSER_AUTH automatically, no explicit passing as an argument is needed. This works because Composer checks the COMPOSER_AUTH environment variable by default and treats it as auth configuration before reading the local auth.json. In .gitlab-ci.yml the variable only needs to be visible in the right scope; the build job reads it without any further action. A common debugging step: run composer config --list in the job (only in debug jobs, never in production), the output shows whether the http-basic credentials for repo.magento.com or other sources are set correctly.

Caching the vendor/ directory in combination with correct Composer auth delivers the best build performance. The cache key should include composer.lock as an input, so a change to the lock file automatically generates a new cache entry. Anyone caching the vendor folder must make sure the cached packages match the current lock file, since a mismatch leads to hard to debug errors because Composer uses the cache blindly. Adding the --no-cache flag to a test job that periodically runs a clean build without cache is good practice.

8. Comparison: Insecure vs. Secure Composer Setup

The difference between an improvised and a secure Composer setup in GitLab is measurable: in audit risk, rotation effort and build reproducibility.

Aspect Insecure Correct Impact
Credential Location auth.json in the repository COMPOSER_AUTH variable No credential leakage through Git history
Key Type Personal developer key Dedicated CI key Rotatable without disrupting the developer workflow
Scope Variable scope * (all branches) Scope production / staging Feature branches never see production keys
Mirror URL https://user:pw@mirror/ in composer.json URL in composer.json, auth in the variable composer.json can be checked in safely
Rotation File update on every server Update the variable in GitLab Rotation possible without server access

The consistent separation of configuration (in the repository) and credentials (in GitLab variables) is the underlying principle that connects every row of the table. Teams that set this up cleanly once spend noticeably less operational effort on onboarding new developers, offboarding former team members and rotating credentials than teams that manage credentials scattered across servers and repositories.

9. Common Error Patterns with Composer Auth in CI

The most common error pattern is Could not authenticate against repo.magento.com. Causes: the COMPOSER_AUTH variable is not set (scope problem), the JSON format is invalid (missing quotes, comma errors), or the access key has expired. Diagnosis: run echo $COMPOSER_AUTH | php -r "echo json_last_error() ? 'invalid JSON' : 'valid JSON';" in the build job to check the format. Then check in the marketplace account whether the access key is still active.

The second error pattern is Package not found for private repositories. Cause: the token does not have the necessary scopes, or the repository URL in composer.json does not match the credential host in the variable. Diagnosis: create the token in the GitLab variable with a minimal read scope for the specific repository, and use composer why-not in the build job to check whether the package source is found at all. The third error pattern is a rate limit error from the Magento Marketplace, recognizable by HTTP 429 responses in the job log. Solution: configure the Composer cache in GitLab correctly, so the same packages are not downloaded again with every build.

# Validate COMPOSER_AUTH and test connectivity in a debug job
debug:composer-auth:
  stage: build
  image: php:8.4-cli
  environment: staging
  script:
    # Validate JSON format of COMPOSER_AUTH without printing the value
    - |
      php -r "
        \$auth = getenv('COMPOSER_AUTH');
        if (!\$auth) { echo 'ERROR: COMPOSER_AUTH not set'; exit(1); }
        json_decode(\$auth);
        if (json_last_error() !== JSON_ERROR_NONE) {
          echo 'ERROR: Invalid JSON in COMPOSER_AUTH: ' . json_last_error_msg();
          exit(1);
        }
        echo 'COMPOSER_AUTH: valid JSON, length=' . strlen(\$auth) . PHP_EOL;
      "
    # Test connectivity to Magento Marketplace (no download, just HEAD request)
    - curl -sf -o /dev/null -w "HTTP %{http_code}" https://repo.magento.com/ || true
  when: manual
  allow_failure: true

10. Summary

Managing Composer Auth, private repositories and mirror configurations securely in GitLab is the foundation of a reproducible Magento build in CI. COMPOSER_AUTH as a masked variable with the JSON content of auth.json: no auth.json in the repository, no manual credential management on servers. Dedicated CI keys instead of personal developer keys for the Magento Marketplace. Environment scope for the variable, so feature branches never see production credentials. Composer cache in GitLab, so packages are not downloaded again with every build.

The goal is a build process where every developer can run the same pipeline without knowing any local credentials, and where rotating a marketplace key or a repository token is a change in GitLab, not a coordination project across three servers and six developer machines. This decoupling is what turns Composer Auth in GitLab from a pragmatic workaround into a lasting standard.

Composer Auth in GitLab: The Essentials at a Glance

Credential Storage

COMPOSER_AUTH as a masked variable, never auth.json in the repository. Validate the JSON format beforehand with jq.

Key Management

Dedicated CI keys for the Magento Marketplace, separate from personal developer keys. Deploy tokens for GitLab internal private repositories.

Scope and Rotation

Define the environment scope narrowly. Rotation happens only in GitLab: no server updates, no code changes.

Mirror and Cache

Mirror URLs in composer.json, credentials in the variable. Configure the Composer cache in GitLab to avoid rate limits.

11. FAQ: Managing Composer Auth Securely in GitLab

1Creating COMPOSER_AUTH in GitLab?
As a CI/CD variable, value = JSON content of auth.json. Set the masked flag. Validate the JSON with jq beforehand.
2Is auth.json allowed in the repository?
No. It ends up in the Git history and is accessible with every clone. Credentials only as a GitLab variable.
3Dedicated CI keys, why?
Rotatable without disrupting the developer workflow, higher rate limits, and clearly tied to the CI process.
4Private GitLab repos in Composer?
Deploy token with read_repository scope as a bearer entry in COMPOSER_AUTH. URL in composer.json, auth in the variable.
5Mirror vs. Private Packagist?
Mirror: caches Packagist packages locally. Private Packagist: hosted private package server. Both reduce external dependency.
6Masked variable fails?
GitLab has a length limit. With too many credentials: use protected instead of masked, or split the credentials.
7Checking COMPOSER_AUTH correctly?
php -r "json_decode(getenv('COMPOSER_AUTH')); echo json_last_error();" prints 0 if the JSON is valid. Never print the value directly.
8Checking repository URLs into composer.json?
Yes, URLs are not secret. Credentials come from COMPOSER_AUTH. Never embed credentials in URLs.
9Avoiding rate limit errors from the Marketplace?
Enable the Composer cache in GitLab, base the cache key on composer.lock, use --prefer-dist.
10Right scope for COMPOSER_AUTH?
If staging and production use the same credentials: one scope is enough. If not: separate variables with the same name and different scope.